Live view of AILANG as it is written: after each edit of a .ail file, a pane shows the module's functions, their types and effect rows (pure or !{IO,FS,...})…

Quick start package for using AILANG with Claude Code and OpenAI Codex.
AILANG is a deterministic programming language designed for AI code synthesis and reasoning.
Install the AILANG CLI before installing either agent integration:
curl -fsSL https://ailang.sunholo.com/install.sh | bash
ailang --version
The plugins invoke the local ailang executable, so it must be available on PATH.
# Step 1: Add the marketplace
/plugin marketplace add sunholo-data/ailang_bootstrap
# Step 2: Install the plugin
/plugin install ailang@sunholo-data/ailang_bootstrap
Or clone and add locally:
git clone https://github.com/sunholo-data/ailang_bootstrap
/plugin marketplace add ./ailang_bootstrap
/plugin install ailang@ailang_bootstrap
Codex requires Node.js 18 or newer for the local MCP server:
node --version
codex plugin marketplace add sunholo-data/ailang_bootstrap --ref stable
codex plugin add ailang@ailang-marketplace
codex plugin list
Start a new Codex session after installation. Alternatively, add the marketplace with the CLI, then install AILANG through /plugins or the Codex desktop Plugins directory.
Codex loads the shared skills and five local MCP tools: ailang_prompt, ailang_check, ailang_run, ailang_builtins, and ailang_eval.
Download a platform-specific release from GitHub Releases:
| Platform | Architecture | Asset |
|---|---|---|
| macOS | Apple Silicon | darwin.arm64.ailang-bootstrap.tar.gz |
| macOS | Intel | darwin.x64.ailang-bootstrap.tar.gz |
| Linux | x64 | linux.x64.ailang-bootstrap.tar.gz |
| Windows | x64 | win32.x64.ailang-bootstrap.zip |
Each release includes the AILANG binary pre-bundled.
| Skill | Purpose |
|---|---|
| ailang | Write, run, and develop with AILANG |
| ailang-inbox | Cross-agent messaging with semantic search, deduplication, and GitHub sync |
| Command | Purpose |
|---|---|
/ailang-prompt | Load syntax teaching prompt (do this first!) |
/ailang-run <file> | Run an AILANG program |
/ailang-check <file> | Type-check without running |
/ailang-new <name> [template] | Create new program from template |
/ailang-repl | Start interactive REPL |
/ailang-builtins [search] | List builtin functions |
/ailang-editor <editor> | Install syntax highlighting |
| Hook | Event | Purpose | |||
|---|---|---|---|---|---|
session_start.sh | SessionStart | Inbox check + brain context | |||
brain_session.sh | SessionStart | Inject relevant brain knowledge | |||
git_guard.sh | PreToolUse(Bash) | Block destructive git ops | |||
microrag_context.sh | PreToolUse(Edit\ | Write\ | Read\ | MultiEdit) | JIT μRAG knowledge injection — surfaces AILANG syntax/builtin docs at tool-call time |
microrag_lint.sh | PostToolUse(Edit\ | Write\ | MultiEdit) | First-use builtin nudge for *.ail edits | |
brain_resolution.sh | PostToolUse(Bash) | Capture command resolutions | |||
observatory_hook.sh | All events | OTEL telemetry for AILANG Observatory |
Disable μRAG: export AILANG_MICRORAG_ENABLED=0. Requires ailang and jq on PATH; degrades silently if either is missing.
μRAG brain corpus auto-bootstrap: install.sh runs ailang micro-rag init && ailang micro-rag bootstrap --scope user --no-embed after the binary lands. This populates the brain DB at ~/.ailang/state/brain.db with the ailang-syntax (~50 frames) and ailang-builtins (~280 frames) namespaces using only resources embedded in the binary — no source repo required. Works on Windows / minimal Docker images / anywhere awk and python3 are absent.
Re-run manually after install.sh to upgrade the corpus to embedding-backed retrieval (requires Ollama):
ailang micro-rag bootstrap --scope user # add embeddings on top of SimHash/FTS
ailang micro-rag bootstrap --scope user --reset # full rebuild (use after AILANG version bump)
A live pane that shows what you're writing as AILANG sees it. After every Edit/Write of a .ail file it runs ailang iface and ailang check and draws the module's functions, their types and effect rows (pure or !{IO,FS}), plus any type errors — keeping the last good signatures on screen beside a broken edit. The status line carries a one-line summary.
/plugin install ailang-lens@ailang-marketplace
/reload-plugins
/ail-lens [file.ail] # open the pane, or analyse any file into it
Mods are an early-access Claude Code feature; where they are not enabled the plugin installs but does nothing. Requires ailang on PATH.
1. Local stdio MCP (auto-installed by this plugin) — wraps the local AILANG CLI for actually running/checking code:
ailang_check - Type-check filesailang_run - Run programsailang_prompt - Get teaching prompt (embedded copy)ailang_builtins - List builtinsailang_eval - Evaluate an expression in the non-interactive REPL2. Remote HTTP MCP at mcp.ailang.sunholo.com — version-locked live docs, stdlib, examples, design docs, benchmarks. Available to any agent harness that supports remote MCP (Claude Desktop, Cursor, Cline, Continue, Claude Code).
{
"mcpServers": {
"ailang-docs": {
"url": "https://mcp.ailang.sunholo.com/mcp/",
"transport": "streamable-http"
}
}
}
Tools include prompt_get, stdlib_modules, stdlib_search, examples_for_concept, limitations_list, effects_catalog, and submit_feedback (for filing AILANG bugs from inside an agent session). Use these to avoid stale embedded knowledge — the snapshot is rebuilt and redeployed on every AILANG release.
See docs/guides/agent-mcp for the full tool catalog.
Once installed, you can:
# CRITICAL: Load syntax before writing code
ailang prompt
# Run AILANG code
ailang run --caps IO --entry main program.ail
# Start interactive REPL
ailang repl
# Type-check without running
ailang check program.ail
module myproject/hello
export func main() -> () ! {IO} {
print("Hello, AILANG!")
}
Run with:
ailang run --caps IO --entry main hello.ail
func - NOT fn, function, or deflet x = 1; let y = 2; x + y=> - NOT : or ->print expects string - Use print(show(42)) for numbersailang_bootstrap/
├── .claude-plugin/
│ ├── plugin.json # Claude Code plugin manifest
│ └── marketplace.json # Skills marketplace
├── .codex-plugin/
│ └── plugin.json # Codex plugin manifest
├── .mcp.json # Codex MCP configuration
├── .claude/commands/ # Slash commands
│ ├── ailang-run.md
│ ├── ailang-check.md
│ ├── ailang-new.md
│ └── ...
├── mcp-server/ # MCP server for AILANG tools
│ ├── server.js
│ └── package.json
├── skills/
│ ├── ailang/ # Main AILANG skill
│ │ ├── SKILL.md
│ │ └── resources/
│ └── ailang-inbox/ # Agent messaging skill
│ └── SKILL.md
├── bin/ # AILANG binary (in releases)
└── .github/workflows/
└── release.yml # Platform-specific release builds
MIT License - see LICENSE
hooks/register.tsx 290 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { LensFunc, LensModule } from '../types'
5
6const PANE = 'ailang-lens'
7const modules = atom({ plugin: 'ailang-lens', key: 'modules' } as const, [])
8
9// First JSON object in a stream that may carry warnings before it.
10function parseJson(text: string): any {
11 const at = text.indexOf('{')
12 if (at < 0) return undefined
13 try {
14 return JSON.parse(text.slice(at))
15 } catch {
16 return undefined
17 }
18}
19
20function typeName(t: unknown): string {
21 if (typeof t === 'string') return t
22 if (t && typeof t === 'object' && 'name' in t) return String((t as { name: unknown }).name)
23 return JSON.stringify(t)
24}
25
26// "type error in m (decl 0): type unification failed at [... at m.ail:3:8]: cannot unify ..."
27// → "3:8 cannot unify ..."
28function shortError(message: string): string {
29 const where = message.match(/\.ail:(\d+:\d+)/)?.[1]
30 const tail = message.includes(']: ')
31 ? message.slice(message.lastIndexOf(']: ') + 3)
32 : message.replace(/^type error in \S+ \(decl \d+\):\s*/, '')
33 return where ? `${where} ${tail}` : message
34}
35
36async function exists($: EngineInterface, path: string): Promise<boolean> {
37 try {
38 await $.fs.stat(path)
39 return true
40 } catch {
41 return false
42 }
43}
44
45// ailang names a module by its path from the working directory (MOD010), so
46// run it from the project root: the nearest ailang.toml, else .git, else the
47// file's own folder.
48async function projectRoot($: EngineInterface, file: string): Promise<string> {
49 const dirs: string[] = []
50 for (let dir = file.slice(0, file.lastIndexOf('/')); dir; dir = dir.slice(0, dir.lastIndexOf('/'))) {
51 dirs.push(dir)
52 }
53 for (const marker of ['ailang.toml', '.git']) {
54 for (const dir of dirs) {
55 if (await exists($, `${dir}/${marker}`)) return dir
56 }
57 }
58 return dirs[0] ?? '.'
59}
60
61async function mtime($: EngineInterface, file: string): Promise<number> {
62 try {
63 return (await $.fs.stat(file)).mtimeMs
64 } catch {
65 return 0
66 }
67}
68
69/** Pure: the files a module imports by relative path (`import ./data/stars`). */
70export function relativeImports(file: string, source: string): string[] {
71 const dir = file.slice(0, file.lastIndexOf('/'))
72 return [...source.matchAll(/^\s*import\s+(\.{1,2}\/[\w./-]+)/gm)].map(m => {
73 const parts = `${dir}/${m[1]}.ail`.split('/')
74 const out: string[] = []
75 for (const part of parts) {
76 if (part === '..') out.pop()
77 else if (part !== '.') out.push(part)
78 }
79 return out.join('/')
80 })
81}
82
83// A module's result goes stale when the module, its package's manifest or
84// lock, or a module it imports by relative path changes: a package-qualified
85// import resolves through ailang.toml's [exports], so a manifest edit can
86// flip a check without the module itself changing.
87async function stampOf($: EngineInterface, file: string, root: string): Promise<number> {
88 let source = ''
89 try {
90 source = await $.fs.read(file)
91 } catch {
92 // gone: the stamp drops to 0 and the next check reports it
93 }
94 const paths = [file, `${root}/ailang.toml`, `${root}/ailang.lock`, ...relativeImports(file, source)]
95 return Math.max(...(await Promise.all(paths.map(path => mtime($, path)))))
96}
97
98async function analyse($: EngineInterface, file: string, previous?: LensModule): Promise<LensModule> {
99 const started = Date.now()
100 const cwd = file.startsWith('/') ? await projectRoot($, file) : undefined
101 const root = cwd ?? '.'
102 const stamp = await stampOf($, file, root)
103 const rel = cwd ? file.slice(cwd.length + 1) : file
104 const [iface, check] = await Promise.all([
105 $.process.run(['ailang', 'iface', rel], { cwd, timeoutMs: 20_000 }),
106 $.process.run(['ailang', 'check', '--json', '--quiet', rel], { cwd, timeoutMs: 60_000 }),
107 ])
108 const shape = iface.exitCode === 0 ? parseJson(iface.stdout) : undefined
109 const report = parseJson(check.stdout)
110
111 const errors: string[] = report?.errors?.length
112 ? report.errors.map((err: { message?: string }) => shortError(err.message ?? 'error'))
113 : check.exitCode === 0
114 ? []
115 : [shortError((check.stderr || check.stdout).trim().split('\n').pop() ?? 'check failed')]
116
117 // A broken edit keeps the last good shape on screen, beside the errors.
118 const funcs: LensFunc[] = shape
119 ? (shape.funcs ?? []).map((f: LensFunc) => ({ name: f.name, type: f.type, effects: f.effects ?? [] }))
120 : (previous?.funcs ?? [])
121
122 return {
123 file,
124 module: shape?.module ?? previous?.module ?? (file.split('/').pop() ?? file).replace(/\.ail$/, ''),
125 types: shape ? (shape.types ?? []).map(typeName) : (previous?.types ?? []),
126 funcs,
127 passed: errors.length === 0 && check.exitCode === 0,
128 errors,
129 ms: Date.now() - started,
130 root,
131 stamp,
132 }
133}
134
135async function refresh($: EngineInterface, file: string): Promise<LensModule> {
136 const known = await read($, modules)
137 const lens = await analyse($, file, known.find(m => m.file === file))
138 await update($, modules, list => [lens, ...list.filter(m => m.file !== file)].slice(0, 8))
139
140 const effects = [...new Set(lens.funcs.flatMap(f => f.effects))].sort()
141 $.ui.status(
142 lens.passed
143 ? `ail ✓ ${lens.module} · ${lens.funcs.length} fn · ${effects.length ? `!{${effects.join(',')}}` : 'pure'}`
144 : `ail ✗ ${lens.module} · ${lens.errors.length} error${lens.errors.length === 1 ? '' : 's'}`,
145 )
146 return lens
147}
148
149// Edit/Write are seen directly; anything else that touches a module, its
150// package manifest or a relative import (Bash, another session, the person's
151// editor) is caught by its stamp after a Bash call and at the end of each turn.
152async function refreshStale($: EngineInterface): Promise<void> {
153 for (const known of await read($, modules)) {
154 if ((await stampOf($, known.file, known.root)) !== known.stamp) await refresh($, known.file)
155 }
156}
157
158// An edited .ail or ailang.toml can change how its package's other modules
159// check (exports, a sibling's signature), so they are re-checked with it.
160async function refreshPackage($: EngineInterface, edited: string): Promise<void> {
161 for (const known of await read($, modules)) {
162 if (known.file !== edited && known.root !== '.' && edited.startsWith(`${known.root}/`)) await refresh($, known.file)
163 }
164}
165
166export const register: Register = on => {
167 on('session.start', async ($, e, next) => {
168 await $.command.register({
169 name: 'ail-lens',
170 description: 'Open the AILANG lens, or analyse a .ail file into it',
171 argumentHint: '[file.ail]',
172 })
173 return next(e)
174 })
175
176 on('command.run', { command: 'ail-lens' }, async ($, e) => {
177 const arg = e.args.trim()
178 if (!arg) {
179 await refreshStale($)
180 await $.ui.open({ id: PANE, title: 'AILANG lens' })
181 return { text: 'AILANG lens opened.' }
182 }
183 if (!arg.endsWith('.ail')) return { text: `ail-lens: ${arg} is not a .ail file.` }
184
185 // A relative path is the person's, typed against the session's folder.
186 const file = arg.startsWith('/') ? arg : `${await $.session.cwd()}/${arg.replace(/^\.\//, '')}`
187 if (!(await exists($, file))) return { text: `ail-lens: ${file} not found.` }
188
189 const lens = await refresh($, file)
190 await $.ui.open({ id: PANE, title: 'AILANG lens' })
191 return { text: `AILANG lens: ${lens.module} ${lens.passed ? '✓' : `✗ ${lens.errors.length} error(s)`}` }
192 })
193
194 // Every successful edit of a .ail file re-reads its interface and type-checks it.
195 on('tool.call', async ($, e, next) => {
196 const ran = await next(e)
197 if (e.tool === 'Bash') {
198 await refreshStale($).catch(() => undefined)
199 return ran
200 }
201 const file = (e.tool === 'Edit' || e.tool === 'Write') ? e.file_path : undefined
202 const isModule = file?.endsWith('.ail') === true
203 const isManifest = file?.endsWith('/ailang.toml') === true
204 if (!file || !(isModule || isManifest) || ran.deny !== undefined || ran.isError) return ran
205
206 try {
207 if (isModule) {
208 await refresh($, file)
209 void $.ui.open({ id: PANE, title: 'AILANG lens' })
210 }
211 await refreshPackage($, file)
212 } catch (err) {
213 $.ui.status(`ail lens: ${String(err).slice(0, 60)}`)
214 }
215 return ran
216 })
217
218 on('turn.complete', async ($, e, next) => {
219 const done = await next(e)
220 await refreshStale($).catch(() => undefined)
221 return done
222 })
223
224 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
225 const { Box, Text } = $.ui.resolve(e)
226 const [current, ...older] = await read($, modules)
227
228 if (!current) {
229 return (
230 <Box flexDirection="column">
231 <Text dimColor>No AILANG edited yet.</Text>
232 <Text dimColor>Edit a .ail file, or /ail-lens path/to/file.ail</Text>
233 </Box>
234 )
235 }
236
237 const width = Math.max(...current.funcs.map(f => f.name.length), 4)
238 return (
239 <Box flexDirection="column">
240 <Text>
241 <Text bold>{current.module}</Text>
242 <Text color={current.passed ? 'green' : 'red'}>{current.passed ? ' ✓ checks' : ' ✗ fails'}</Text>
243 <Text dimColor>{` ${current.ms}ms`}</Text>
244 </Text>
245
246 {current.types.length > 0 && (
247 <Text>
248 <Text dimColor>types </Text>
249 <Text color="magenta">{current.types.join(' ')}</Text>
250 </Text>
251 )}
252
253 <Box flexDirection="column" marginTop={1}>
254 {current.funcs.map(f => (
255 <Box flexDirection="column">
256 <Text>
257 <Text bold>{f.name.padEnd(width)}</Text>
258 <Text color={f.effects.length ? 'yellow' : 'green'}>
259 {f.effects.length ? ` !{${f.effects.join(',')}}` : ' pure'}
260 </Text>
261 </Text>
262 <Text dimColor wrap="truncate-end">{` ${f.type}`}</Text>
263 </Box>
264 ))}
265 </Box>
266
267 {current.errors.length > 0 && (
268 <Box flexDirection="column" marginTop={1}>
269 {current.errors.map(err => (
270 <Text color="red" wrap="wrap">{`✗ ${err}`}</Text>
271 ))}
272 {current.funcs.length > 0 && <Text dimColor>(signatures above are the last good shape)</Text>}
273 </Box>
274 )}
275
276 {older.length > 0 && (
277 <Box flexDirection="column" marginTop={1}>
278 <Text dimColor>recent</Text>
279 {older.map(m => (
280 <Text dimColor>
281 {`${m.passed ? '✓' : '✗'} ${m.module} · ${m.funcs.length} fn`}
282 </Text>
283 ))}
284 </Box>
285 )}
286 </Box>
287 )
288 })
289}
290types/index.d.ts 22 lines1export type LensFunc = { name: string; type: string; effects: string[] }
2
3export type LensModule = {
4 file: string
5 module: string
6 types: string[]
7 funcs: LensFunc[]
8 passed: boolean
9 errors: string[]
10 ms: number
11 /** Package root the checks ran from (nearest ailang.toml, else .git). */
12 root: string
13 /** Newest mtime of the module, its package manifest/lock and its ./ imports. */
14 stamp: number
15}
16
17declare module 'claude-code' {
18 interface PluginState {
19 'ailang-lens': { modules: LensModule[] }
20 }
21}
22