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
★ 34v0.1.0Apache-2.0updated 2026-10-02sunholo-data/ailang/tools/claude-mods/ailang-inbox-band
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

<img src="docs/static/img/ailang-logo.svg" alt="AILANG Logo" width="128" height="128">

AILANG: The Deterministic Language for AI Coders

<!-- EXAMPLES_STATUS_START --> Examples License

Example status <!-- EXAMPLES_STATUS_END -->

Reliability Security Maintainability CodeQL OpenSSF Scorecard OpenSSF Best Practices

Third-party verification. AILANG is written autonomously by AI agents via its own coordinator. The badges above are independent static-analysis and supply-chain scores — not self-reported. The benchmark dashboard publishes the live correctness signal across current benchmark and model cohorts.

AILANG is a purely functional, effect-typed language designed as a deterministic execution substrate for AI-generated code. Every construct has deterministic semantics that can be reflected, verified, and serialized.

Documentation | Examples | Live Demos | Vision | Benchmarks


Quick Start

AILANG is designed to be used by AI coding agents. The AILANG Bootstrap packages the current language guidance, reusable skills, and local MCP tools for Claude Code and OpenAI Codex.

Install the AILANG CLI first:

curl -fsSL https://ailang.sunholo.com/install.sh | bash
ailang --version

The agent integrations call the local ailang executable, so it must be available on PATH.

With Claude Code

/plugin marketplace add sunholo-data/ailang_bootstrap
/plugin install ailang@sunholo-data/ailang_bootstrap

With OpenAI Codex

AILANG Bootstrap also ships a Codex plugin manifest. Add its stable marketplace:

# Required by the plugin's local MCP server on first launch
node --version
npm --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, after adding the marketplace, launch codex, enter /plugins, and install ailang from the AILANG marketplace. The plugin can also be installed from the Plugins directory in the Codex desktop app. Codex plugins are not currently available in the IDE extension.

Codex reads this repository's AGENTS.md automatically, whether or not the plugin is installed. The plugin adds the reusable AILANG skills and MCP tools; AGENTS.md supplies repository-specific workflows and guardrails.

What the Agent Package Provides

  • CLI integration - Skills and tools use the separately installed ailang executable
  • MCP tools - ailang_prompt, ailang_check, ailang_run, ailang_builtins, ailang_eval
  • Agent skills - AILANG authoring, debugging, messaging, design-doc, and sprint workflows
  • Teaching guidance - Current syntax rules via repository guidance, skills, and ailang prompt

The packaging is intentionally native to each host:

CapabilityClaude CodeCodex
Persistent guidanceCLAUDE.mdAGENTS.md
Reusable workflowsPlugin skillsPlugin skills
Local AILANG MCP toolsYesYes
Host-native commandsClaude slash commandsSkills and MCP tools
AILANG Bootstrap hooksClaude hooksNot currently packaged

Once installed, ask the agent to write AILANG code. All agents should run ailang prompt before authoring .ail files; it is the installed CLI's source for current syntax and idioms.

See ailang_bootstrap for details.

The installer detects your OS and architecture automatically. To pin a release:

curl -fsSL https://ailang.sunholo.com/install.sh | VERSION=v0.33.1 bash
# macOS (Apple Silicon)
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/darwin.arm64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/

# macOS (Intel)
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/darwin.x64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/

# Linux
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/linux.x64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/

# From source
git clone https://github.com/sunholo-data/ailang.git
cd ailang && make install

# Verify
ailang --version

For complete setup instructions, see the Getting Started Guide.

Hello World

module examples/hello_world

import std/io (println)

export func main() -> () ! {IO} {
  println("Hello, World!")
}
ailang run --caps IO examples/hello_world.ail
# Output: Hello, World!

Interactive REPL

ailang repl

λ> 1 + 2
3 :: Int

λ> let double = \x. x * 2 in double(21)
42 :: Int

λ> :type \x. x + x
\x. x + x :: ∀α. Num α ⇒ α → α

λ> :quit

Key Features

  • Pure functional - Lambda calculus, closures, pattern matching, ADTs
  • Type inference - Hindley-Milner with row polymorphism
  • Effect system - Capability-based security (IO, FS, Net, Clock, AI)
  • Deterministic - Replayable execution, structured traces
  • AI-first - Designed for machine reasoning, not human convenience
  • LSP - ailang lsp --stdio ships in the binary: diagnostics, hover types, go-to-def, references, document symbols. One-command VS Code install: ailang editor install vscode (guide)

Learn more: Why AILANG? | No Loops Design | Go Interop | Stability Promise (1.x)


Development

make install    # Build and install
make test       # Run all tests
make repl       # Start REPL
make lint       # Run linter

Guides:


Project Structure

ailang/
├── cmd/ailang/     # CLI
├── internal/       # Compiler (lexer, parser, types, eval, effects)
├── std/            # Standard library (std/io, std/fs, std/json, std/zip, std/xml, etc.)
├── examples/       # Example and reference programs
├── docs/           # Documentation website source
└── design_docs/    # Design documents

License

Apache 2.0 - See LICENSE

AILANG draws inspiration from Haskell, OCaml, Rust, and Idris/Agda.


For AI agents: Deterministic functional language with Hindley-Milner type inference, algebraic effects, and explicit effect tracking. Read AGENTS.md, then use ailang prompt for current language guidance.

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