Mascot artwork is a derivative of gajae-code's branding — see assets/NOTICE.md for attribution.
Your AI shouldn't live in a browser tab. It should live in your DMs.
gajae-way is a runtime that turns an AI coding agent into a resident persona — one that sits in your Discord, Telegram, and Slack, remembers you in plain Markdown, wakes itself up on a schedule, and keeps every conversation in its own private head. No new place to check for ordinary conversation — you just talk to it where you already talk. An optional local gajaeway-admin console exists for the operator, not for everyday chat.
Discord / Telegram / Slack ──> gajaeway gateway ──> your persona (gjc)
│
Markdown memory + scheduled/event monitors
Chat frontends are easy. What is hard is everything that happens when a real bot has to stay up for months: crashes mid-reply, two messages racing the same conversation, an agent that forgets who you are, an agent that confuses one room for another, an agent that answers a message you never sent. gajae-way is the boring, durable layer that handles those — so the persona on top can be interesting.
- One presence, many rooms — never one blur. Every conversation is a validated origin (
platform/kind/conversationId) with its own session and epoch. Your DM is not your team channel./newrebinds that conversation and leaves the others untouched. - Memory you can read with
cat. Turns and monitor output land in$GAJAEWAY_HOME/memoryas Markdown across a small set of canonical axes. It is your filesystem, your git history, your grep — not a vector blob you have to trust. - It acts without being asked. Cron and event monitors give the persona its own turns, so it can canonicalize memory on a schedule or audit itself each morning while you sleep.
- Delivery is ledgered, not hoped for. Replies get a durable record before they go out. After a crash, a send whose outcome was uncertain is reissued and visibly labeled a duplicate instead of quietly pretending nothing happened.
- It knows when to shut up. A
dmPolicy-accepted DM engages without needing a mention. Group traffic is mention-gated unless you explicitly open a channel — and an opened channel still lets the persona choose silence over noise. - Safety floors gjc always sees. Every persona turn carries non-configurable ActionGuard system guidance forbidding unrecoverable commands (recursive removal of
/, filesystem formatting, raw device writes, fork bombs) and recursive deletion of$HOMEitself or absolute paths outside$HOMEand$GAJAEWAY_HOME; the gateway does not execute or intercept persona commands itself. A configured monitor script is separately checked against the same floors before it runs. - Standalone binaries, not a stack.
bun run buildemits compiled executables. Production hosts run those under launchd/systemd; the source checkout stays on your laptop.
- Send a DM that your
dmPolicyaccepts (owner-only,allowlist, oropen) and talk normally. Your bot responds in that conversation's own ongoing context. - In a group, it stays out of the way until you mention it. You can explicitly open a configured channel for normal conversation.
- Send
/newwhen you want a fresh start in that conversation. It confirms that a fresh session has started. - When the gateway accepts a DM or an addressed group turn (a real mention or native reply), Discord and Slack both show a reaction-gradient presence — phase markers, an advancing clock, and an effort digit (tool calls, falling back to output tokens) reacted directly onto the message it is answering; Discord also shows the platform's native typing indicator, and Slack also drives Slack's native assistant status line (
<app> <status>, viaassistant.threads.setStatus) under the reply thread. Slack additionally promotes messages in an adapter-configuredopenchannel to addressed; Discord does not treatopen-channel messages as addressed without a real mention. The reaction-gradient markers themselves are never a posted or edited message. Presence stays up through mid-work messages while the turn keeps working and comes off only when the turn ends. In a channel it merely overhears, nothing is shown until it actually replies. - Replies are protected by a durable delivery record. After a crash, a send whose outcome was uncertain may be reissued; when it was, it is visibly labeled as a duplicate rather than silently pretending it was not.
- Conversations and useful monitor output are captured under your own
$GAJAEWAY_HOME/memorydirectory as readable Markdown, not hidden in a proprietary store.
The Bun workspace under packages/ is divided by responsibility:
| Package | Responsibility |
|---|---|
@gajae-gateway/protocol |
Versioned NDJSON frames, negotiation, verb/event catalogues, and canonical origins. |
@gajae-gateway/sdk |
Client for the gateway's Unix-domain socket or stdio transport. |
@gajae-gateway/cli |
Owner commands over the gateway socket. |
@gajae-gateway/gateway |
Daemon: configuration, SQLite state, sessions, delivery, memory, monitors, and the gjc boundary. |
@gajae-gateway/adapter-discord |
Discord ingress and outbound delivery, including typing hints and the reaction-gradient presence. |
@gajae-gateway/adapter-telegram |
Telegram ingress and outbound delivery. |
@gajae-gateway/adapter-slack |
Slack ingress over Socket Mode, mrkdwn delivery, reactions, reaction-gradient working presence, opt-in live replies, and missed-message recovery. |
@gajae-gateway/protocol, @gajae-gateway/sdk, and @gajae-gateway/cli are npm-publishable as standalone packages — see Publishing packages below for the release workflow. The gateway, adapters, admin console, and @gajae-gateway/subsession stay private application code and ship only as the compiled binaries below; @gajae-gateway/conformance is a private, unshipped CI/test-only package.
For how memory, monitors, and the gateway work end to end, see the documentation.
-
Build the standalone programs on a machine with Bun:
bun run build
This creates
dist/gajaeway-gateway,dist/gajaeway-discord,dist/gajaeway-telegram,dist/gajaeway-slack,dist/gajaeway-admin, anddist/gajaeway. -
Choose a private home directory and create
$GAJAEWAY_HOME/config.jsonplus separate credential files. The gateway configuration references credential files, rather than storing secret values inline. See deployment for the complete layout and examples. -
Create
$GAJAEWAY_HOME/adapter-discord.jsonwith its owntokenFilereference for Discord. Create the analogous Telegram adapter configuration when using Telegram, oradapter-slack.jsonwithbotTokenFileandappTokenFilereferences when using Slack. -
Run the gateway as a long-lived daemon under your service manager:
dist/gajaeway-gateway daemon
The host also needs the external
gjcexecutable, either resolvable onPATHor pinned with an absoluteGJC_EXECUTABLE; it supplies the AI runtime for every turn. -
Start the Discord adapter, then send your bot a DM:
dist/gajaeway-discord
Start
dist/gajaeway-telegramordist/gajaeway-slackseparately when using Telegram or Slack.
Put SOUL.md, AGENTS.md, and USER.md in $GAJAEWAY_HOME/workspace. SOUL.md and USER.md are read for each turn. gjc discovers AGENTS.md when a session starts; a later edit or deletion supersedes that project-context copy on the next turn. The workspace is also your persona's working directory. Keep the home directory private: it contains configuration, the gateway database, your workspace, and memory.
- Deployment guide — configuration, credentials, and service-manager setup
- Memory guide — readable memory and search
- Monitor guide — scheduled and event-driven work
- Architecture — protocol and reliability details
- Operator runbook — recovery and troubleshooting
This repository is a Bun/TypeScript workspace.
bun install
bun run build # compiles gateway, adapters, admin, and CLI into standalone binaries under dist/
bun test packages # runs the full package test suiteProduction hosts run the compiled binaries, not this source checkout.
@gajae-gateway/protocol, @gajae-gateway/sdk, and @gajae-gateway/cli are npm-publishable under the @gajae-gateway scope. Each has its own build step that bundles src/ into a single Node-ESM-resolvable dist/*.js with Bun, then emits .d.ts declarations with tsc (emitDeclarationOnly), and a prepublishOnly script so a publish always ships current compiled output. The published tarball includes both dist/ (resolved by main/types/the default export condition for non-Bun consumers) and src/ (resolved by the bun export condition, so an installed Bun consumer imports source directly instead of the compiled build). @gajae-gateway/protocol has zero runtime dependencies. Packing must go through Bun, not plain npm: npm pack --dry-run only lists a package's contents and does not rewrite dependencies — plain npm pack/npm publish of a package directory leave workspace:* in the packed manifest, which a real npm consumer cannot resolve. bun pm pack rewrites each published package's workspace:* dependency on @gajae-gateway/protocol/@gajae-gateway/sdk to the exact version being packed, producing a registry-valid manifest — verified by packing with bun pm pack, npm install-ing the tarballs into a scratch project, and running a real node --input-type=module -e "import('@gajae-gateway/sdk')" against the installed package (transcript: artifacts/ultragoal-npm-clean-install-verification.txt).
scripts/release-packages.ts runs the whole release in the required dependency order (protocol → sdk → cli, since sdk and cli depend on the versions published before them):
bun run release:dry-run # builds each package, then `bun pm pack`s it into dist-packed/ — never touches the registry
bun run release:publish # builds and packs each package, then `npm publish`es that tarball (add --tag <name> for a non-latest dist-tag)Inspect dist-packed/*.tgz from a dry run before ever running release:publish. Bun takes the rewritten version from the workspace entries in bun.lock, which bun install does not refresh after a package.json version bump; the script refuses any tarball that pins a sibling package to a version other than the one being released, so bump those bun.lock entries together with the manifests. The upload itself uses the npm CLI because only npm supports trusted publishing. @gajae-gateway/sdk and @gajae-gateway/cli depend on @gajae-gateway/protocol/@gajae-gateway/sdk being published first, which is why the script releases them in that order. Each package's publishConfig sets public npm access for the scoped name.
build.yml runs on every push and pull request: Biome, tsc --noEmit, bun test packages/, and the frozen-corpus benchmark gate, each across macOS arm64, Linux x64, and Linux arm64.
release.yml runs on a v*.*.* tag push (or manually via workflow_dispatch, with an optional npm_tag input for a non-latest dist-tag). It re-runs the full verify matrix first, then only on green: publishes @gajae-gateway/protocol, @gajae-gateway/sdk, and @gajae-gateway/cli to npm in dependency order via bun run release:publish, builds and archives the six standalone binaries for each platform, and creates a GitHub release with the archives attached and auto-generated notes. Publishing uses npm trusted publishing (OIDC), not a token: each of the three packages has a GitHub Actions trusted publisher on npmjs.com for repository Yeachan-Heo/gajae-way and workflow release.yml, the job holds id-token: write, and npm >= 11.5.1 exchanges the job's OIDC token for a short-lived publish credential and attaches provenance. No NPM_TOKEN secret is used. Publishing works only from GitHub-hosted runners and only from release.yml; renaming the workflow file requires updating the trusted publisher on all three packages.
Copyright (C) 2026 Yeachan Heo. Licensed under the GNU Affero General Public License v3.0.
