SLOPSHOPPER

ailang-lens

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,...})…

newpaneguardcommandstatusprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ailang-lens
│ ┃ AILANG lens ✕ › fix the failing auth test and add an audit log call │ ┃ No AILANG edited yet. │ ┃ Edit a .ail file, or /ail-lens ⏺ Read(src/auth.ts) │ ┃ path/to/file.ail ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /ail-lens │ ⎿ ailang-lens: AILANG lens opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · AILANG lens
No AILANG edited yet. Edit a .ail file, or /ail-lens path/to/file.ail
README

AILANG Bootstrap

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.

Installation

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.

Claude Code

# 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

OpenAI Codex

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.

Manual Installation

Download a platform-specific release from GitHub Releases:

PlatformArchitectureAsset
macOSApple Silicondarwin.arm64.ailang-bootstrap.tar.gz
macOSInteldarwin.x64.ailang-bootstrap.tar.gz
Linuxx64linux.x64.ailang-bootstrap.tar.gz
Windowsx64win32.x64.ailang-bootstrap.zip

Each release includes the AILANG binary pre-bundled.

What's Included

Skills

SkillPurpose
ailangWrite, run, and develop with AILANG
ailang-inboxCross-agent messaging with semantic search, deduplication, and GitHub sync

Slash Commands (Claude Code)

CommandPurpose
/ailang-promptLoad 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-replStart interactive REPL
/ailang-builtins [search]List builtin functions
/ailang-editor <editor>Install syntax highlighting

Hooks (Claude Code)

HookEventPurpose
session_start.shSessionStartInbox check + brain context
brain_session.shSessionStartInject relevant brain knowledge
git_guard.shPreToolUse(Bash)Block destructive git ops
microrag_context.shPreToolUse(Edit\Write\Read\MultiEdit)JIT μRAG knowledge injection — surfaces AILANG syntax/builtin docs at tool-call time
microrag_lint.shPostToolUse(Edit\Write\MultiEdit)First-use builtin nudge for *.ail edits
brain_resolution.shPostToolUse(Bash)Capture command resolutions
observatory_hook.shAll eventsOTEL 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)

AILANG lens (Claude Code mod, optional)

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.

MCP Servers (TWO complementary servers)

1. Local stdio MCP (auto-installed by this plugin) — wraps the local AILANG CLI for actually running/checking code:

  • ailang_check - Type-check files
  • ailang_run - Run programs
  • ailang_prompt - Get teaching prompt (embedded copy)
  • ailang_builtins - List builtins
  • ailang_eval - Evaluate an expression in the non-interactive REPL

2. 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.

Quick Start

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

Example AILANG Program

module myproject/hello

export func main() -> () ! {IO} {
  print("Hello, AILANG!")
}

Run with:

ailang run --caps IO --entry main hello.ail

Key Syntax Rules

  1. Use func - NOT fn, function, or def
  2. Semicolons between statements - let x = 1; let y = 2; x + y
  3. Pattern matching uses => - NOT : or ->
  4. No loops - Use recursion instead
  5. print expects string - Use print(show(42)) for numbers

Documentation

Repository Structure

ailang_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

License

MIT License - see LICENSE

Source 2 files
hooks/register.tsx 290 lines
1import { 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}
290
types/index.d.ts 22 lines
1export 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