Unread AILANG fleet messages above the prompt: a band with the count and newest title, an /ail-inbox pane with Ack, a toast when one arrives. For the person…

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 158 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { InboxMessage } from '../types'
5
6// For the person only (M-CLAUDE-CODE-MODS C3): message text is external
7// content from other agents and GitHub, so nothing here reaches the model.
8// The /ail-inbox command's reply carries a count, never a title or body.
9
10const PANE = 'ailang-inbox-band'
11const unread = atom({ plugin: 'ailang-inbox-band', key: 'unread' } as const, [])
12const toasted = atom({ plugin: 'ailang-inbox-band', key: 'toasted' } as const, [])
13const openId = atom({ plugin: 'ailang-inbox-band', key: 'openId' } as const, '')
14const error = atom({ plugin: 'ailang-inbox-band', key: 'error' } as const, '')
15
16type Raw = { id: string; from_agent?: string; to_inbox?: string; title?: string; payload?: string; created_at?: string }
17
18export function parseList(stdout: string): InboxMessage[] {
19 // The JSON starts on its own line; a version warning may precede it.
20 const at = stdout.search(/^(\[|null)/m)
21 if (at < 0) return []
22 const list = JSON.parse(stdout.slice(at)) as Raw[] | null
23 return (list ?? []).map(m => ({
24 id: m.id,
25 from: m.from_agent ?? '?',
26 inbox: m.to_inbox ?? '?',
27 title: (m.title ?? '').trim() || '(no title)',
28 body: (m.payload ?? '').slice(0, 1500),
29 createdAt: m.created_at ?? '',
30 }))
31}
32
33export function age(createdAt: string, now: number): string {
34 const ms = now - Date.parse(createdAt)
35 if (!Number.isFinite(ms) || ms < 0) return ''
36 const minutes = Math.floor(ms / 60_000)
37 if (minutes < 60) return `${minutes}m`
38 const hours = Math.floor(minutes / 60)
39 return hours < 48 ? `${hours}h` : `${Math.floor(hours / 24)}d`
40}
41
42function inboxesOf(options: Record<string, unknown>): string[] {
43 return String(options.inboxes ?? 'user').split(',').map(s => s.trim()).filter(Boolean)
44}
45
46// The backlog at the first poll of a load is shown in the band, not toasted.
47let hasPolled = false
48
49async function poll($: EngineInterface, inboxes: readonly string[]): Promise<InboxMessage[]> {
50 const found: InboxMessage[] = []
51 for (const inbox of inboxes) {
52 try {
53 const run = await $.process.run(
54 ['ailang', 'messages', 'list', '--unread', '--json', '--limit', '20', '--inbox', inbox],
55 { timeoutMs: 10_000 },
56 )
57 if (run.exitCode !== 0) throw new Error(run.stderr.trim().split('\n').pop() || `exit ${run.exitCode}`)
58 found.push(...parseList(run.stdout))
59 } catch (err) {
60 await update($, error, () => `${inbox}: ${String(err).slice(0, 120)}`)
61 return read($, unread)
62 }
63 }
64 await update($, error, () => '')
65
66 const byNewest = [...new Map(found.map(m => [m.id, m])).values()]
67 .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
68 await update($, unread, () => byNewest)
69
70 // The backlog at the first poll is shown in the band, not toasted.
71 const seen = new Set(await read($, toasted))
72 const fresh = byNewest.filter(m => !seen.has(m.id))
73 if (hasPolled && fresh.length > 0) {
74 $.ui.toast(fresh.length === 1 ? `📬 ${fresh[0]!.from}: ${fresh[0]!.title}` : `📬 ${fresh.length} new messages`)
75 }
76 hasPolled = true
77 await update($, toasted, list => [...list, ...fresh.map(m => m.id)].slice(-500))
78 return byNewest
79}
80
81async function ack($: EngineInterface, id: string): Promise<void> {
82 const run = await $.process.run(['ailang', 'messages', 'ack', id], { timeoutMs: 10_000 })
83 if (run.exitCode !== 0) {
84 $.ui.toast(`ack failed: ${run.stderr.trim().slice(0, 80)}`)
85 return
86 }
87 await update($, unread, list => list.filter(m => m.id !== id))
88}
89
90export const register: Register = (on, options) => {
91 const inboxes = inboxesOf(options)
92 const pollMs = Math.max(10, Number(options.pollSeconds ?? 60)) * 1000
93 hasPolled = false
94
95 on('session.start', async ($, e, next) => {
96 await $.command.register({ name: 'ail-inbox', description: 'Show unread AILANG messages' })
97 void poll($, inboxes)
98 $.clock.every(pollMs, () => void poll($, inboxes))
99 return next(e)
100 })
101
102 on('command.run', { command: 'ail-inbox' }, async $ => {
103 const list = await poll($, inboxes)
104 await $.ui.open({ id: PANE, title: 'AILANG inbox' })
105 return { text: `${list.length} unread in ${inboxes.join(', ')}.` }
106 })
107
108 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
109 const list = await read($, unread)
110 if (e.props.hasSurvey || list.length === 0) return next(e)
111
112 const { Box, Button, Text } = $.ui.resolve(e)
113 const newest = list[0]!
114 return (
115 <Box>
116 <Text>
117 <Text color="cyan">{`📬 ${list.length} unread`}</Text>
118 <Text dimColor>{` · ${newest.inbox} — `}</Text>
119 <Text wrap="truncate-end">{newest.title}</Text>
120 </Text>
121 <Text> </Text>
122 <Button key="open" label="Open" dimColor onPress={() => $.ui.open({ id: PANE, title: 'AILANG inbox' })} />
123 </Box>
124 )
125 })
126
127 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
128 const { Box, Button, Text } = $.ui.resolve(e)
129 const [list, opened, failure, now] = await Promise.all([
130 read($, unread), read($, openId), read($, error), $.clock.now(),
131 ])
132
133 return (
134 <Box flexDirection="column">
135 {failure !== '' && <Text color="red">{`inbox: ${failure}`}</Text>}
136 {list.length === 0 && <Text dimColor>{`No unread messages in ${inboxes.join(', ')}.`}</Text>}
137 {list.map(m => (
138 <Box flexDirection="column" marginBottom={1}>
139 <Text bold wrap="truncate-end">{m.title}</Text>
140 <Box>
141 <Text dimColor>{`${m.from} → ${m.inbox} · ${age(m.createdAt, now)} `}</Text>
142 <Button
143 key={`read-${m.id}`}
144 label={opened === m.id ? 'Hide' : 'Read'}
145 dimColor
146 onPress={() => update($, openId, id => (id === m.id ? '' : m.id))}
147 />
148 <Text> </Text>
149 <Button key={`ack-${m.id}`} label="Ack" onPress={() => ack($, m.id)} />
150 </Box>
151 {opened === m.id && <Text wrap="wrap">{m.body}</Text>}
152 </Box>
153 ))}
154 </Box>
155 )
156 })
157}
158types/index.d.ts 20 lines1export type InboxMessage = {
2 id: string
3 from: string
4 inbox: string
5 title: string
6 body: string
7 createdAt: string
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'ailang-inbox-band': {
13 unread: InboxMessage[]
14 toasted: string[]
15 openId: string
16 error: string
17 }
18 }
19}
20