Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
assistant identity -- as the author or committer on any commit.
- **Never** include `https://claude.ai/code/session_*` links in commit
messages.
- **Never** add a "Generated with Claude Code" line, a session link, or any
similar trailer at the bottom of a commit message.

### Branch naming -- version, then type, then name
- **Never** prefix a branch with `claude/` -- or any other AI / assistant
name.
- Name every branch `version/type/name`: the version first, then the change
type (`feat`, `fix`, `docs`, `chore`, ...), then a short slug. For example
`v0.0.1/feat/ripgrep-coinflip-defaults`. The trailing name may be dropped
for a trivial change (`v0.0.1/feat`). Versions start at `v0.0.1`.

### No AI attribution anywhere
Nothing committed to this repository may advertise that it was produced with
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ FROM python:3.12-slim-bookworm

WORKDIR /app

# ripgrep backs the agent shell tool's content search; grep stays as a
# fallback. ca-certificates and libstdc++6 are runtime dependencies.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates libstdc++6 \
&& apt-get install -y --no-install-recommends \
ca-certificates libstdc++6 ripgrep \
&& rm -rf /var/lib/apt/lists/*

# The Node runtime, lifted from the build image, runs the agent sidecar.
Expand Down
70 changes: 20 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,17 @@ runs the test suite.
- **File workspace** -- the `files.*` and `shell.run` tools give the model a
private scratch directory, one per server (per user in a DM). It is
sandboxed: paths cannot escape it, files and total size are capped, and the
shell runs only an allowlist of read-only commands. Configured with the
`WORKSPACE_*` variables; turn it off entirely with `WORKSPACE_ENABLED`.
shell runs only an allowlist of read-only commands -- it searches with
ripgrep, falling back to grep only when ripgrep cannot serve. Configured
with the `WORKSPACE_*` variables; turn it off entirely with
`WORKSPACE_ENABLED`.
- **Lua plugins** -- a full plugin system. A plugin is one `.lua` file that
can register prefix commands, agent tools, background loops and event
handlers, and reach out through an HTTP client, a Discord read/write API,
document and key/value stores, and JSON utilities. Plugins install from a
GitHub marketplace, survive restarts, and are managed live with
`.ai plugins`.
- **Productivity** -- private notes, tasks organised into to-do lists, and
calendar events with reminders, all delivered as bundled Lua plugins.
Personal items stay private (answered in your DMs); groups let members
share and collaborate, and any item can be shared, copied, moved or
transferred between users and groups.
document and key/value stores, and JSON utilities. The `coinflip` plugin
ships bundled as a worked example; more plugins -- a notes, tasks, events
and groups productivity suite among them -- install from a GitHub
marketplace, survive restarts, and are managed live with `.ai plugins`.
- **Memory sidecar** -- long-term facts and episodes, passive learning in
opted-in channels, and an append-only training corpus of every turn.
- **Thread or inline replies** -- each member picks their style with
Expand All @@ -73,43 +71,15 @@ commands require the Manage Server permission.
| `.arch ctx [@user\|server\|clear]` | everyone | Inspect / wipe learned context. |
| `.arch save` / `saved` / `unsave` | everyone | Bookmark Archimedes answers. |
| `.arch optin` / `optout` | everyone | AI context tracking. |
| `.note` | everyone | Private notes (answered in your DMs). |
| `.task` | everyone | Tasks and to-do lists, with optional reminders. |
| `.event` | everyone | Calendar events with optional reminders. |
| `.group` | everyone | Create groups, invite members, share and transfer items. |
| `.coinflip` | everyone | Flip a coin (the example plugin). |
| `.coinflip` | everyone | Flip a coin (the bundled example plugin). |
| `.ai` | Manage Server | The AI control surface (see `.ai help`). |
| `.ai plugins` | Manage Server | Install, update, enable and disable Lua plugins. |
| `/help` or `.help` | everyone | A menu of sections, every command with examples. |
| `.ping` / `.about` | everyone | Latency and bot info. |

The `.note`, `.task`, `.event`, `.group` and `.coinflip` commands are not
built in -- they come from bundled Lua plugins (see **Lua plugins** below).

### Productivity, privacy and groups

Notes, tasks and events each have an owner. Personal items are yours alone:
the bot replies in your DMs and tidies the command message away, and personal
data follows you across every server. Use `.note share <id> @user [edit]` to
let specific people see one of your items.

A **group** is a shared space. Create one with `.group create <name>`, invite
members with `.group invite <id> @user`, and they accept with
`.group join <id>`. Every member can see and edit the group's items, and group
responses post in the channel so members see them. You can be in many groups.

Targeting and moving items:

- `#<groupid>` at the start of an `add` / `list` argument targets a group
(for example `.note add #5 Buy supplies`); no `#` means your personal space.
- `~<list>` targets a task list (`.task add ~shopping milk`); the default
list is `general`.
- `.note copy <id> <dest>` and `.note move <id> <dest>` accept `me`, an
`@user`, or `#<groupid>` as the destination. `.group duplicate <id>` clones
a whole group's items into a fresh group you own.
- Reminders: `.task remind <id> in 2h` or `.event remind <id> 2026-06-01 14:30`.
Times accept relative offsets (`in 30m`, `in 3d`, `in 1w`) or absolute
`YYYY-MM-DD [HH:MM]` in UTC; a one-minute loop DMs you when one falls due.
The `.coinflip` command is not built in -- it comes from the bundled
`coinflip` Lua plugin (see **Lua plugins** below). Productivity commands like
`.note`, `.task`, `.event` and `.group` install from the plugin marketplace.

## Setup

Expand Down Expand Up @@ -161,15 +131,15 @@ A plugin is a single `.lua` file. It can register prefix commands (with
nested subcommand groups), agent tools the model can call, and background
loops -- with no Python.

Files in `plugins/` are **bundled** plugins, loaded on every boot: `notes`,
`tasks`, `events` and `groups` are the productivity suite, and `coinflip` is
a worked example. `plugins/README.md` documents the plugin contract and the
`arch` / `ctx` API; the per-plugin document store means a plugin never writes
SQL.
The `plugins/` directory holds the **bundled** plugins, loaded on every
boot. Only `coinflip` ships bundled -- a small, complete worked example.
`plugins/README.md` documents the plugin contract and the `arch` / `ctx`
API; the per-plugin document store means a plugin never writes SQL.

More plugins install from a marketplace -- an ordinary GitHub repository
(`hilleywyn/archimedes-plugins` by default). Server moderators manage every
plugin with `.ai plugins`:
(`hilleywyn/archimedes-plugins` by default) -- among them a notes, tasks,
events and groups productivity suite. Server moderators manage every plugin
with `.ai plugins`:

| Command | What |
|---|---|
Expand Down Expand Up @@ -260,7 +230,7 @@ ai/ model client, memory, traits, context, tools, safety
cogs/ chat brain, .arch, .ai admin, sidecar, meta
agent-sidecar/ the OpenRouter Agent SDK service (TypeScript / Node)
database/schema.sql idempotent schema, applied on boot
plugins/ bundled Lua plugins (notes, tasks, events, groups, coinflip)
plugins/ bundled Lua plugins (coinflip, the worked example)
tests/ offline smoke tests
.github/workflows/ CI (lint + tests)
Dockerfile container build
Expand Down
8 changes: 6 additions & 2 deletions ai/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,8 +677,11 @@ def _register_workspace_tools(reg: ToolRegistry) -> None:
reg.register(ToolSpec(
"shell.run",
"Run a single read-only shell command inside your sandboxed "
"workspace, for example ls, cat, grep, find, wc, head, tail or "
"sort. Commands take a file path as a direct argument -- "
"workspace, for example ls, cat, rg, find, wc, head, tail or "
"sort. To search file contents always use rg (ripgrep) -- it is "
"the preferred search tool; only fall back to grep, egrep or "
"fgrep if ripgrep fails or genuinely cannot do what you need. "
"Commands take a file path as a direct argument -- "
"'sort -r data.txt', 'wc -l data.txt' -- so you do NOT need "
"pipes or redirects, which are not supported. To save a "
"command's output to a file, pass save_to instead of using '>'. "
Expand All @@ -687,6 +690,7 @@ def _register_workspace_tools(reg: ToolRegistry) -> None:
{"type": "object", "properties": {
"command": {"type": "string",
"description": "The command line to run, e.g. "
"'rg TODO src' or "
"'sort -r data.txt'."},
"save_to": {"type": "string",
"description": "Optional workspace-relative file "
Expand Down
8 changes: 5 additions & 3 deletions ai/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,12 @@ def delete_file(ctx, path: str) -> dict:
# ── allowlist shell tool ──────────────────────────────────────────────────────
# Read-only commands only. The command is executed directly (no shell), so a
# pipe or redirect is never interpreted -- it is just a literal argument.
# rg (ripgrep) is the preferred content search; grep, egrep and fgrep stay on
# the allowlist only as a fallback for the rare case ripgrep cannot serve.
_SHELL_ALLOWLIST = frozenset({
"ls", "cat", "head", "tail", "wc", "grep", "egrep", "fgrep", "find",
"pwd", "echo", "date", "stat", "sort", "uniq", "cut", "tr", "nl",
"basename", "dirname", "du", "diff",
"ls", "cat", "head", "tail", "wc", "rg", "grep", "egrep", "fgrep",
"find", "pwd", "echo", "date", "stat", "sort", "uniq", "cut", "tr",
"nl", "basename", "dirname", "du", "diff",
})
# find actions (and their write-to-file siblings) run another program or
# write outside a relative path -- never allowed, whatever the command is.
Expand Down
46 changes: 20 additions & 26 deletions cogs/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def build_help_categories(p: str) -> dict[str, list[discord.Embed]]:
f"**Chat** -- `@`mention Archimedes or reply to one of its "
f"messages.\n"
f"**Commands** -- prefix commands start with `{p}` "
f"(for example `{p}note`, `{p}task`).",
f"(for example `{p}help`, `{p}coinflip`).",
)
.field(
"Bot meta",
Expand All @@ -64,9 +64,9 @@ def build_help_categories(p: str) -> dict[str, list[discord.Embed]]:
)
.field(
"Good to know",
"Personal notes, tasks and events are private: Archimedes "
"answers them in your DMs and tidies the command away. Group "
"items are shared and answered in the channel.",
"Archimedes can be extended with Lua plugins that add extra "
"commands and tools. Server moderators install them; see the "
"Plugins section below.",
),
], "Getting started")

Expand Down Expand Up @@ -171,31 +171,25 @@ def build_help_categories(p: str) -> dict[str, list[discord.Embed]]:
"Plugins",
color=C_TEAL,
description=(
"Most of what Archimedes does for you -- notes, tasks, "
"calendar events, shareable groups -- is delivered by Lua "
"plugins. The Notes, Tasks, Events and Groups sections of "
"this menu are generated live from the plugins installed "
"right now, so the help always matches what is loaded."
"Archimedes is extended with Lua plugins -- each one adds "
"prefix commands, agent tools the model can call, or both. "
"The `coinflip` plugin ships built in as a worked example; "
"this menu grows a live section for every plugin that is "
"loaded, so the help always matches what is installed."
),
)
.field(
"Scope tokens",
f"`#<groupid>` at the start of an `add` / `list` argument files "
f"the item in a group; no `#` means your personal space.\n"
f"`~<list>` picks a task list (the default list is `general`).\n"
f"Example: `{p}task add #5 ~launch ship the build`",
"What a plugin adds",
"A plugin can register prefix commands you run yourself and "
"agent tools Archimedes calls for you mid-conversation. Every "
"loaded plugin gets its own section in this menu.",
)
.field(
"Private vs shared",
"Personal notes, tasks and events are answered in your DMs and "
"the command message is tidied away. Group items are shared with "
"every member and answered in the channel.",
)
.field(
"Time formats",
"Relative: `in 30m`, `in 2h`, `in 3d`, `in 1w`. "
"Absolute (UTC): `2026-06-01` or `2026-06-01 14:30`. "
"Tasks and events can carry a reminder that DMs you when due.",
"The marketplace",
f"Beyond the built-in `coinflip`, more plugins -- a notes, "
f"tasks, events and groups productivity suite among them -- "
f"install from the marketplace. Browse it with "
f"`{p}ai plugins search`.",
)
.field(
"Managing plugins",
Expand Down Expand Up @@ -258,8 +252,8 @@ def _catalogue(self) -> dict[str, list[discord.Embed]]:
"""The static help plus a live section for every loaded plugin.

Plugin sections slot in right after the static ``Plugins`` page so
the menu reads: built-in topics, then one section per installed
plugin (Notes, Tasks, ...), then the staff controls.
the menu reads: built-in topics, then one section per loaded plugin
(coinflip, plus anything installed), then the staff controls.
"""
prefix = Config.PREFIX
static = build_help_categories(prefix)
Expand Down
10 changes: 6 additions & 4 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ nested subcommands), agent tools the model can call, and background loops,
without touching any Python.

Files in this directory are **bundled** plugins: they ship with the bot and
are loaded on every boot. More plugins can be installed from the marketplace
are loaded on every boot. Only `coinflip` ships bundled -- it is the worked
example installed out of the box. More plugins install from the marketplace
with `.ai plugins install <id>`.

## The plugin contract
Expand Down Expand Up @@ -303,6 +304,7 @@ picture can post it by replying with a card whose `image` is the result URL.

## Trying it

See `coinflip.lua` for a complete worked example, and the `notes`, `tasks`,
`events` and `groups` plugins for a full suite that shares one namespace.
After editing a bundled file, run `.ai plugins reload <id>`.
See `coinflip.lua` for a complete worked example. After editing a bundled
file, run `.ai plugins reload <id>`. More plugins -- including a notes,
tasks, events and groups productivity suite that shares one namespace --
install from the marketplace with `.ai plugins install <id>`.
Loading
Loading