SLOPSHOPPER

ailang-inbox-band

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…

newpanebandcommandtoastprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ailang-inbox-band
│ ┃ AILANG inbox ✕ › fix the failing auth test and add an audit log call │ ┃ No unread messages in user. │ ⏺ Read(src/auth.ts) │ ⎿ 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-inbox │ ⎿ ailang-inbox-band: 0 unread in user. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · AILANG inbox
No unread messages in user.
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 158 lines
1import { 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}
158
types/index.d.ts 20 lines
1export 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