MoonBit Code Transformations
SkillDev toolsWriting MoonBit source-to-source code transformations using moonbitlang/parser and its fmt package. Use when parsing MoonBit source into AST, constructing new AST nodes, or emitting generated MoonBit code.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the MoonBit Code Transformations skill
What this skill tells your AI
The instructions your AI receives, as published by golemcloud/golem in .agents/skills/moonbit-code-transform/SKILL.md and read by ahel’s review.
Guide for building source-to-source code generation tools in MoonBit using
moonbitlang/parser to parse source into AST and moonbitlang/parser/fmt to
emit generated code. Uses golem_sdk_tools as the reference implementation.
Pipeline Overview
Every code transformation follows the same pattern:
Source (.mbt files)
→ Parse with @parser.parse_string()
→ Extract information from @syntax.Impl list
→ Construct new @syntax.Impl nodes (AST)
→ Format with @formatter.impls_to_string()
→ Write output file
Required Dependencies
In moon.mod.json:
{
"deps": {
"moonbitlang/parser": "0.3.18",
"moonbitlang/lexer": "0.3.15"
}
}
In the library package moon.pkg (where AST construction happens):
import {
"moonbitlang/core/list",
"moonbitlang/parser",
"moonbitlang/parser/syntax",
"moonbitlang/parser/basic",
}
import {
"moonbitlang/parser/fmt" @formatter,
} for "test"
In the CLI/main package moon.pkg (where formatting and file I/O happen):
import {
"moonbitlang/parser/fmt" @formatter,
"moonbitlang/x/fs",
"your/module/lib" @lib,
}
Key separation: The library package constructs @list.List[@syntax.Impl].
The CLI package calls @formatter.impls_to_string(impls) and writes the result.
Tests in the library import moonbitlang/parser/fmt as @formatter via for "test" to verify
output without depending on it at runtime.
Parsing Source Files
Use @parser.parse_string() to parse MoonBit source into a list of top-level
AST items:
let (impls, _reports) = @parser.parse_string(content)
// impls : @list.List[@syntax.Impl]
// _reports : Array[...] — diagnostics, usually ignored for codegen
Walking the AST to Extract Information
Pattern-match on @syntax.Impl variants to find declarations:
impls.each(fn(impl_) {
match impl_ {
// Function declarations
TopFuncDef(fun_decl~, ..) => {
// fun_decl : @syntax.FunDecl
let name = fun_decl.name.name // String
let vis = fun_decl.vis // @syntax.Visibility
let params = fun_decl.decl_params // Option[@list.List[@syntax.Parameter]]
let ret = fun_decl.return_type // Option[@syntax.Type]
let doc = fun_decl.doc // @syntax.DocString
let type_name = fun_decl.type_name // Option[@syntax.TypeName] (for methods)
...
}
// Type definitions (struct, enum)
TopTypeDef(type_decl) => {
let name = type_decl.tycon // String
let attrs = type_decl.attrs // @list.List[@syntax.Attribute]
let doc = type_decl.doc // @syntax.DocString
...
}
_ => ()
}
})
Checking Attributes (Annotations)
Attributes like #derive.agent are accessed via type_decl.attrs:
let mut is_agent = false
type_decl.attrs.each(fn(attr) {
if attr.raw == "#derive.agent" {
is_agent = true
}
})
Extracting Function Parameters
Parameters come as @list.List[@syntax.Parameter]:
match fun_decl.decl_params {
Some(ps) =>
ps.each(fn(p) {
match p {
Positional(binder~, ty=Some(ty)) | Labelled(binder~, ty=Some(ty)) => {
let name = binder.name // String
// ty : @syntax.Type — process recursively
}
_ => ()
}
})
None => ()
}
Processing Types Recursively
@syntax.Type is an enum with variants like Name, Option, Tuple, etc.:
fn process_type(ty : @syntax.Type) -> MyTypeRepr {
match ty {
Option(ty~, ..) => MyOptional(process_type(ty))
Name(constr_id~, tys~, ..) =>
match constr_id.id {
Ident(name~) =>
if name == "Array" {
match tys {
More(inner, ..) => MyList(process_type(inner))
Empty => ... // error
}
} else if tys.is_empty() {
MySimple(name)
} else { ... }
Dot(pkg~, id~) => MyQualified(pkg, id)
}
_ => ... // unsupported
}
}
Constructing AST Nodes
All AST nodes require a loc field. For generated code, use dummy locations:
let dummy_pos : @basic.Position = { fname: "", lnum: 0, bol: 0, cnum: 0 }
let dummy_loc : @basic.Location = { start: dummy_pos, end: dummy_pos }
The @list.List Pattern
The AST uses @list.List[T] (immutable linked lists), not Array[T].
Build with Array first, then convert:
fn[T] to_list(arr : Array[T]) -> @list.List[T] {
@list.List::from_array(arr)
}
DSL Helper Functions
Create a set of small helper functions to make AST construction readable.
Organize by category. The reference implementation is in
golem_sdk_tools/lib/ast_helpers.mbt.
Primitives
fn make_binder(name : String) -> @syntax.Binder {
@syntax.Binder::{ name, loc: dummy_loc }
}
fn make_label(name : String) -> @syntax.Label {
@syntax.Label::{ name, loc: dummy_loc }
}
Type Constructors
// Simple type: String, Int, MyStruct, etc.
fn make_type(name : String) -> @syntax.Type {
@syntax.Type::Name(
constr_id=@syntax.ConstrId::{
id: @syntax.LongIdent::Ident(name~),
loc: dummy_loc,
},
tys=to_list([]),
loc=dummy_loc,
)
}
// Generic type: Array[T], Result[T, E], etc.
fn make_parameterized_type(
name : String,
type_args : Array[@syntax.Type],
) -> @syntax.Type {
@syntax.Type::Name(
constr_id=@syntax.ConstrId::{
id: @syntax.LongIdent::Ident(name~),
loc: dummy_loc,
},
tys=to_list(type_args),
loc=dummy_loc,
)
}
// Option type: T?
fn make_option_type(inner : @syntax.Type) -> @syntax.Type {
@syntax.Type::Option(ty=inner, loc=dummy_loc, question_loc=dummy_loc)
}
Literal Expressions
fn make_string_expr(s : String) -> @syntax.Expr {
@syntax.Expr::Constant(c=@syntax.Constant::String(s), loc=dummy_loc)
}
fn make_int_expr(n : Int) -> @syntax.Expr {
@syntax.Expr::Constant(c=@syntax.Constant::Int(n.to_string()), loc=dummy_loc)
}
fn make_bool_expr(b : Bool) -> @syntax.Expr {
@syntax.Expr::Constant(c=@syntax.Constant::Bool(b), loc=dummy_loc)
}
Other numeric types use their respective @syntax.Constant variants:
UInt("0"), Int64("0"), UInt64("0"), Float("0.0"), Double("0.0"),
Byte("\\x00"), Char('a').
Identifier Expressions
// Simple identifier: foo
fn make_ident_expr(name : String) -> @syntax.Expr {
@syntax.Expr::Ident(
id=@syntax.Var::{ name: @syntax.LongIdent::Ident(name~), loc: dummy_loc },
loc=dummy_loc,
)
}
// Qualified identifier: @pkg.foo
fn make_qualified_expr(pkg : String, id : String) -> @syntax.Expr {
@syntax.Expr::Ident(
id=@syntax.Var::{ name: @syntax.LongIdent::Dot(pkg~, id~), loc: dummy_loc },
loc=dummy_loc,
)
}
// Method reference: TypeName::method_name
fn make_method_ref(type_name : String, method_name : String) -> @syntax.Expr {
@syntax.Expr::Method(
type_name=@syntax.TypeName::{
name: @syntax.LongIdent::Ident(name=type_name),
is_object: false,
loc: dummy_loc,
},
method_name=make_label(method_name),
loc=dummy_loc,
)
}
Constructor Expressions
// Enum variant without args: None, Ok, Err, etc.
fn make_constr_no_args(name : String) -> @syntax.Expr {
@syntax.Expr::Constr(
constr=@syntax.Constructor::{
name: @syntax.ConstrName::{ name, loc: dummy_loc },
extra_info: @syntax.ConstructorExtraInfo::NoExtraInfo,
loc: dummy_loc,
},
loc=dummy_loc,
)
}
// Qualified enum variant: @pkg.Type::Variant
fn make_qualified_constr(
pkg : String,
type_name : String,
variant : String,
) -> @syntax.Expr {
@syntax.Expr::Constr(
constr=@syntax.Constructor::{
name: @syntax.ConstrName::{ name: variant, loc: dummy_loc },
extra_info: @syntax.ConstructorExtraInfo::TypeName(@syntax.TypeName::{
name: @syntax.LongIdent::Dot(pkg~, id=type_name),
is_object: false,
loc: dummy_loc,
}),
loc: dummy_loc,
},
loc=dummy_loc,
)
}
Function Application
fn make_positional_arg(expr : @syntax.Expr) -> @syntax.Argument {
@syntax.Argument::{ value: expr, kind: @syntax.ArgumentKind::Positional }
}
// f(arg1, arg2, ...)
fn make_apply(func : @syntax.Expr, args : Array[@syntax.Expr]) -> @syntax.Expr {
@syntax.Expr::Apply(
func~,
args=to_list(args.map(make_positional_arg)),
attr=@syntax.ApplyAttr::NoAttr,
loc=dummy_loc,
)
}
// self.method(arg1, arg2, ...)
fn make_dot_apply(
self_ : @syntax.Expr,
method_name : String,
args : Array[@syntax.Expr],
) -> @syntax.Expr {
@syntax.Expr::DotApply(
self=self_,
method_name=make_label(method_name),
args=to_list(args.map(make_positional_arg)),
return_self=false,
attr=@syntax.ApplyAttr::NoAttr,
loc=dummy_loc,
)
}
// lhs op rhs (e.g., a + b, x == y)
fn make_infix(
op : String,
lhs : @syntax.Expr,
rhs : @syntax.Expr,
) -> @syntax.Expr {
@syntax.Expr::Infix(
op=@syntax.Var::{ name: @syntax.LongIdent::Ident(name=op), loc: dummy_loc },
lhs~,
rhs~,
loc=dummy_loc,
)
}
Collections and Composites
// Record literal: { field1: expr1, field2: expr2, ... }
fn make_record(fields : Array[@syntax.FieldDef]) -> @syntax.Expr {
@syntax.Expr::Record(
type_name=None,
fields=to_list(fields),
trailing=@syntax.TrailingMark::Comma,
loc=dummy_loc,
)
}
fn make_field(name : String, expr : @syntax.Expr) -> @syntax.FieldDef {
@syntax.FieldDef::{
label: make_label(name),
expr,
is_pun: false,
loc: dummy_loc,
}
}
// Array literal: [elem1, elem2, ...]
fn make_array(elems : Array[@syntax.Expr]) -> @syntax.Expr {
@syntax.Expr::Array(exprs=to_list(elems), loc=dummy_loc)
}
// Tuple: (a, b, c)
fn make_tuple(elems : Array[@syntax.Expr]) -> @syntax.Expr {
@syntax.Expr::Tuple(exprs=to_list(elems), loc=dummy_loc)
}
// Array indexing: arr[idx]
fn make_array_get(array : @syntax.Expr, index : @syntax.Expr) -> @syntax.Expr {
@syntax.Expr::ArrayGet(array~, index~, loc=dummy_loc)
}
// Type constraint: (expr : Type)
fn make_constraint_expr(expr : @syntax.Expr, ty : @syntax.Type) -> @syntax.Expr {
@syntax.Expr::Constraint(expr~, ty~, loc=dummy_loc)
}
Control Flow
// let name = expr; body
fn make_let(
name : String,
expr : @syntax.Expr,
body : @syntax.Expr,
) -> @syntax.Expr {
@syntax.Expr::Let(
pattern=@syntax.Pattern::Var(make_binder(name)),
expr~,
body~,
loc=dummy_loc,
)
}
// let name : Type = expr; body
fn make_let_typed(
name : String,
ty : @syntax.Type,
expr : @syntax.Expr,
body : @syntax.Expr,
) -> @syntax.Expr {
@syntax.Expr::Let(
pattern=@syntax.Pattern::Constraint(
pat=@syntax.Pattern::Var(make_binder(name)),
ty~,
loc=dummy_loc,
),
expr~,
body~,
loc=dummy_loc,
)
}
// raise expr
fn make_raise(err_value : @syntax.Expr) -> @syntax.Expr {
@syntax.Expr::Raise(err_value~, loc=dummy_loc)
}
// guard cond else { otherwise }; body
fn make_guard_else(
cond : @syntax.Expr,
otherwise : @syntax.Expr,
body : @syntax.Expr,
) -> @syntax.Expr {
@syntax.Expr::Guard(cond~, otherwise=Some(otherwise), body~, loc=dummy_loc)
}
// try { body } catch { cases... }
fn make_try_catch(
body : @syntax.Expr,
catch_cases : Array[@syntax.Case],
) -> @syntax.Expr {
@syntax.Expr::Try(
body~,
catch_=to_list(catch_cases),
catch_all=false,
try_else=None,
has_try=true,
try_loc=dummy_loc,
catch_loc=dummy_loc,
else_loc=dummy_loc,
loc=dummy_loc,
)
}
fn make_case(pattern : @syntax.Pattern, body : @syntax.Expr) -> @syntax.Case {
@syntax.Case::{ pattern, body, guard_: None }
}
// fn(p1, p2) { body }
fn make_lambda(
param_names : Array[String],
body : @syntax.Expr,
) -> @syntax.Expr {
let parameters = param_names.map(fn(name) {
@syntax.Parameter::Positional(binder=make_binder(name), ty=None)
})
@syntax.Expr::Function(
func=@syntax.Func::{
body,
error_type: @syntax.ErrorType::NoErrorType,
has_error: None,
is_async: None,
kind: @syntax.FnKind::Lambda,
loc: dummy_loc,
parameters: to_list(parameters),
params_loc: dummy_loc,
return_type: None,
},
loc=dummy_loc,
)
}
Sequences and Blocks
// Handles both single expression and multi-expression sequences
fn make_sequence(exprs : Array[@syntax.Expr]) -> @syntax.Expr {
if exprs.length() == 1 {
exprs[0]
} else {
let n = exprs.length()
let init : Array[@syntax.Expr] = []
for i in 0..<(n - 1) {
init.push(exprs[i])
}
@syntax.Expr::Sequence(
exprs=to_list(init),
last_expr=exprs[n - 1],
loc=dummy_loc,
)
}
}
Important: @syntax.Expr::Sequence separates the last expression from the
rest. For a single expression, just return it directly — no Sequence wrapper.
Top-Level Nodes
// fn init { body }
fn make_init_block(body : @syntax.Expr) -> @syntax.Impl {
@syntax.Impl::TopExpr(
expr=body,
is_main=false,
local_types=to_list([]),
is_async=None,
loc=dummy_loc,
)
}
// Typed parameter for fn declarations
fn make_param(name : String, type_name : String) -> @syntax.Parameter {
@syntax.Parameter::Positional(
binder=make_binder(name),
ty=Some(make_type(type_name)),
)
}
// pub fn name(params...) -> ReturnType { body }
fn make_top_fn(
name : String,
params : Array[@syntax.Parameter],
return_type : @syntax.Type,
body : @syntax.Expr,
) -> @syntax.Impl {
@syntax.Impl::TopFuncDef(
fun_decl=@syntax.FunDecl::{
type_name: None,
name: make_binder(name),
has_error: None,
is_async: None,
decl_params: Some(to_list(params)),
params_loc: dummy_loc,
quantifiers: to_list([]),
return_type: Some(return_type),
error_type: @syntax.ErrorType::NoErrorType,
vis: @syntax.Visibility::Pub(attr=None, loc=dummy_loc),
attrs: to_list([]),
doc: @syntax.DocString::empty(),
},
decl_body=@syntax.DeclBody::DeclBody(
local_types=to_list([]),
expr=body,
),
loc=dummy_loc,
)
}
Formatting Output
The final step converts AST nodes back to source code:
let impls : @list.List[@syntax.Impl] = ... // constructed AST
let formatted : String = @formatter.impls_to_string(impls)
let output = "// Generated by my-tool — DO NOT EDIT!\n\n" + formatted
@formatter.impls_to_string() produces properly formatted MoonBit source with
///| block separators.
Real-World Examples
Example 1: Generating Function Re-exports
Goal: Parse .mbt source files, find pub fn declarations, generate
wrapper functions that delegate to the original package.
Input: Source file with pub fn wasmExportSave() -> Int { 42 }
Output:
///|
pub fn wasmExportSave() -> Int {
@gen.wasmExportSave()
}
Implementation (from golem_sdk_tools/lib/reexports.mbt):
pub fn generate_reexports(
fns : Array[FnSignature],
gen_pkg : String,
) -> @list.List[@syntax.Impl] {
let impls : Array[@syntax.Impl] = []
for fn_ in fns {
let params : Array[@syntax.Parameter] = []
let args : Array[@syntax.Argument] = []
for j, param_type in fn_.params {
let name = "p\{j}"
params.push(make_param(name, param_type))
args.push(make_positional_arg(make_ident_expr(name)))
}
let call_expr = make_apply(
make_qualified_expr(gen_pkg, fn_.gen_name),
args.map(fn(a) { a.value }),
)
let fun_decl : @syntax.FunDecl = { ... } // see source for full details
impls.push(@syntax.Impl::TopFuncDef(fun_decl~, ...))
}
to_list(impls)
}
Example 2: Generating Agent Registration Code
Goal: Find structs annotated with #derive.agent, extract their ::new
constructor signatures, and generate an fn init { ... } block with
register_agent(...) calls.
Input: User-written agent code:
///| A counter agent
#derive.agent
pub(all) struct Counter {
name : String
mut value : UInt64
}
///| Creates a new counter
pub fn Counter::new(name : String) -> Counter {
{ name, value: 0 }
}
Output (generated golem_agents.mbt):
fn init {
@agents.register_agent({
name: "Counter",
agent_type: { type_name: "Counter", description: "A counter agent", ... },
construct: fn(input) {
try {
let elements = @extractor.extract_tuple(input)
guard elements.length() == 1 else {
raise @common.AgentError::InvalidInput(
"Expected 1 elements, got " + elements.length().to_string(),
)
}
let name : String = @schema.from_element_value_as(elements[0])
Ok(Counter::new(name))
} catch {
e => Err(e)
}
},
})
}
Implementation pattern (from golem_sdk_tools/lib/agents_emit.mbt):
The construct function body is built inside-out, starting with the innermost
expression (Ok(Counter::new(args...))) and wrapping outward:
fn build_construct_fn(agent : AgentInfo) -> @syntax.Expr {
// 1. Build innermost: Ok(AgentName::new(p1, p2, ...))
let ok_call = make_apply(make_constr_no_args("Ok"), [
make_apply(make_method_ref(agent.name, "new"), new_args),
])
// 2. Wrap with let bindings (from last parameter to first)
let mut body = ok_call
for idx in 0..<param_count {
let i = param_count - 1 - idx
body = make_let_typed(name, ty, deserialize_expr, body)
}
// 3. Wrap with guard for parameter count check
body = make_guard_else(length_check, raise_error, body)
// 4. Wrap with let elements = extract_tuple(input)
let try_body = make_let("elements", extract_call, body)
// 5. Wrap in fn(input) { try { ... } catch { e => Err(e) } }
make_lambda(["input"], make_try_catch(try_body, [catch_case]))
}
Key insight: Build AST expressions inside-out. make_let(name, expr, body)
nests — the body of one let contains the next let. Start with the
innermost expression and wrap outward.
Testing Code Transformations
Use snapshot tests with @formatter.impls_to_string() to verify output:
test "generate_reexports produces correct output" {
let fns : Array[FnSignature] = [
{ gen_name: "wasmExportSave", user_name: "wasmExportSave",
params: [], return_type: "Int" },
]
let impls = generate_reexports(fns, "gen")
let output = @formatter.impls_to_string(impls)
inspect(
output,
content=(
#|///|
#|pub fn wasmExportSave() -> Int {
#| @gen.wasmExportSave()
#|}
#|
#|
),
)
}
Run moon test --update to auto-populate the content= parameter, then review
the snapshot to confirm correctness.
For the parsing side, test extraction separately:
test "parse_agents finds simple agent struct" {
let content =
#|///| A counter agent
#|#derive.agent
#|pub(all) struct Counter {
#| name : String
#| mut value : UInt64
#|}
#|
#|///| Creates a new counter
#|pub fn Counter::new(name : String) -> Counter {
#| { name, value: 0 }
#|}
let agents = parse_agents([("counter.mbt", content)])
inspect(agents[0].name, content="Counter")
inspect(agents[0].constructor_params[0].1, content="Simple(\"String\")")
}
Key AST Types Reference
@syntax.Impl — Top-Level Items
| Variant | Purpose |
|---|---|
TopFuncDef(fun_decl~, decl_body~, loc~) | Function definition (pub fn ...) |
TopExpr(expr~, is_main~, local_types~, ..) | Init/main block (fn init { ... }) |
TopTypeDef(type_decl) | Type definition (struct, enum) |
TopLetDef(...) | Top-level let binding |
TopTest(...) | Test block |
@syntax.Expr — Expressions (Most Common Variants)
| Variant | Generates |
|---|---|
Constant(c~, loc~) | Literals: "hello", 42, true |
Ident(id~, loc~) | Identifiers: foo, @pkg.bar |
Method(type_name~, method_name~, loc~) | Method refs: Counter::new |
Constr(constr~, loc~) | Constructors: None, Ok, @pkg.Type::Variant |
Apply(func~, args~, attr~, loc~) | Function call: f(a, b) |
DotApply(self~, method_name~, args~, ..) | Method call: x.foo(a) |
Infix(op~, lhs~, rhs~, loc~) | Binary op: a + b, x == y |
Let(pattern~, expr~, body~, loc~) | Let binding: let x = e; body |
Array(exprs~, loc~) | Array literal: [a, b, c] |
Tuple(exprs~, loc~) | Tuple: (a, b) |
Record(type_name~, fields~, ..) | Record: { x: 1, y: 2 } |
ArrayGet(array~, index~, loc~) | Index: arr[i] |
Constraint(expr~, ty~, loc~) | Type annotation: (e : T) |
Guard(cond~, otherwise~, body~, loc~) | Guard: guard c else { ... }; body |
Raise(err_value~, loc~) | Raise error: raise e |
Try(body~, catch_~, ..) | Try-catch: try { ... } catch { ... } |
Function(func~, loc~) | Lambda: fn(x) { body } |
Sequence(exprs~, last_expr~, loc~) | Multi-statement: a; b; c |
Field(record~, accessor~, loc~) | Field access: r.field |
@syntax.Constant — Literal Variants
| Variant | Example |
|---|---|
String(String) | "hello" |
Int(String) | 42 (note: string representation) |
UInt(String) | 42U |
Int64(String) | 42L |
UInt64(String) | 42UL |
Float(String) | 1.0F |
Double(String) | 1.0 |
Bool(Bool) | true |
Byte(String) | b'\x00' |
Char(String) | 'a' |
@syntax.LongIdent — Identifiers
| Variant | Represents |
|---|---|
Ident(name~) | Simple: foo |
Dot(pkg~, id~) | Qualified: @pkg.foo |
@syntax.FunDecl — Function Declaration Fields
| Field | Type | Purpose |
|---|---|---|
name | Binder | Function name |
type_name | Option[TypeName] | Some(T) for T::method |
vis | Visibility | Pub(..) or Priv |
decl_params | Option[List[Parameter]] | Parameters |
return_type | Option[Type] | Return type |
doc | DocString | Doc comments |
attrs | List[Attribute] | Attributes/annotations |
quantifiers | List[...] | Type parameters |
error_type | ErrorType | Error type annotation |
Common Patterns
Inside-Out Expression Building
When generating nested let bindings, build from the innermost expression
outward. Each make_let wraps the previous body:
let mut body = final_expr
for i = params.length() - 1; i >= 0; i = i - 1 {
body = make_let(params[i].name, params[i].init, body)
}
// body is now: let p0 = ...; let p1 = ...; final_expr
Multiple Statements in a Block
Use make_sequence to combine multiple expressions into a block body.
For fn init { stmt1; stmt2; ... }:
let calls = agents.map(fn(a) { build_register_call(a) })
make_init_block(make_sequence(calls))
Generated File Convention
Always prefix generated files with a comment and write to a well-known filename:
let output = "// Generated by my-tool — DO NOT EDIT!\n\n" + formatted
@fs.write_string_to_file("\{target_dir}/my_generated.mbt", output)
CLI Entry Point Pattern
fn main {
let args = @env.args()
if args.length() < 2 { println("Usage: ..."); return }
if args[1] == "my-command" {
run_my_command(args[2]) catch { e => println("Error: \{e}") }
}
}
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 2k
- Forks
- 212
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
moonbit-code-transform- Source
- github.com/golemcloud/golem