Skip to content

Commit 05a1d9d

Browse files
committed
codex on imessage
A coding agent that helps you build and ship with AI — powered by ChatGPT, now on iMessage. Paste your OpenAI key, connect Spectrum, get a phone number, text Codex from your iPhone. - Stateful Codex conversation per user, threaded via the OpenAI Responses API. Send /new to reset. - OpenAI keys and Spectrum project secrets AES-256-GCM encrypted at rest. - One Docker image, two processes (webapp + bridge daemon).
0 parents  commit 05a1d9d

43 files changed

Lines changed: 4118 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.git
2+
.github
3+
.gitignore
4+
.next
5+
node_modules
6+
.env
7+
.env.local
8+
.env*.local
9+
README.md
10+
Dockerfile
11+
.dockerignore
12+
docker-compose.yml
13+
*.log
14+
.vscode
15+
.idea

.env.example

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Public URL the app is reachable at (no trailing slash).
2+
PUBLIC_URL=http://localhost:3000
3+
4+
# Spectrum dashboard host — exposes the device-auth flow and project APIs.
5+
SPECTRUM_API_HOST=https://app.photon.codes
6+
7+
# Spectrum runtime host — exposes project-scoped user/platform/token APIs.
8+
SPECTRUM_RUNTIME_HOST=https://spectrum.photon.codes
9+
10+
# Device-auth client_id. Must be in the operator's ALLOWED_DEVICE_CLIENT_IDS
11+
# allowlist on the Spectrum dashboard. `photon-cli` is the entry shipped with
12+
# the public Photon CLI and works out of the box.
13+
SPECTRUM_CLIENT_ID=photon-cli
14+
15+
# Postgres connection string.
16+
DATABASE_URL=postgres://postgres:postgres@localhost:5432/codex
17+
18+
# 32-byte hex master key for encrypting tenant OpenAI keys at rest.
19+
# Generate once: openssl rand -hex 32
20+
MASTER_KEY=
21+
22+
# Default model used to answer iMessage threads.
23+
CODEX_MODEL=gpt-5-codex
24+
25+
# Container entrypoint mode: "webapp" or "bridge".
26+
PROCESS=webapp

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @KumarVandit

.github/workflows/ci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
concurrency:
10+
group: ci-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
check:
15+
name: Lint & Type Check
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v5
19+
20+
- uses: oven-sh/setup-bun@v2
21+
with:
22+
bun-version: "1.3"
23+
24+
- name: Install dependencies
25+
run: bun install --frozen-lockfile
26+
27+
- name: Type check
28+
run: bun run typecheck
29+
30+
- name: Lint & format (Biome)
31+
run: bunx biome ci .
32+
33+
build:
34+
name: Build
35+
runs-on: ubuntu-latest
36+
needs: [check]
37+
steps:
38+
- uses: actions/checkout@v5
39+
40+
- name: Build
41+
uses: photon-hq/buildspace/.github/blocks/typescript-build@main
42+
with:
43+
bun-version: "1.3"
44+
build-command: "bun run build"

.github/workflows/release.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
jobs:
8+
release:
9+
uses: photon-hq/buildspace/.github/workflows/typescript-service-release.yaml@main
10+
permissions:
11+
contents: write
12+
pull-requests: read
13+
with:
14+
service-name: codex
15+
bun-version: "1.3"
16+
build-command: "bun run build"
17+
no-npm-publish: true
18+
secrets:
19+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
20+
APP_ID: ${{ secrets.APP_ID }}
21+
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}

.gitignore

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# dependencies
2+
node_modules/
3+
.pnp
4+
.pnp.*
5+
6+
# next
7+
.next/
8+
out/
9+
10+
# production
11+
build/
12+
dist/
13+
14+
# misc
15+
.DS_Store
16+
*.pem
17+
18+
# debug
19+
npm-debug.log*
20+
yarn-debug.log*
21+
yarn-error.log*
22+
.pnpm-debug.log*
23+
24+
# env
25+
.env
26+
.env.local
27+
.env*.local
28+
29+
# typescript
30+
*.tsbuildinfo
31+
tsconfig.tsbuildinfo
32+
next-env.d.ts
33+
34+
# bun — lockfile IS committed for reproducible `bun install --frozen-lockfile`
35+
# in Docker builds. Only the legacy binary lockfile is ignored.
36+
bun.lockb
37+
38+
# IDE
39+
.vscode/
40+
.idea/
41+
42+
# local test/screenshot artifacts (Playwright MCP, ad-hoc captures)
43+
.playwright-mcp/
44+
*-stage.png
45+
test-results/
46+
playwright-report/
47+
48+
# drizzle
49+
drizzle/migrations.json
50+
51+
# deploy state (per-platform, never committed)
52+
.railway/
53+
railway.json
54+
railway.toml
55+
.vercel/
56+
.fly/
57+
fly.toml
58+
deploy/
59+
.deploy-state

Dockerfile

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
ARG BUN_VERSION=1.3
2+
ARG NODE_VERSION=22
3+
4+
FROM oven/bun:${BUN_VERSION}-alpine AS deps
5+
WORKDIR /app
6+
COPY package.json bun.lock* ./
7+
RUN bun install --frozen-lockfile
8+
9+
FROM oven/bun:${BUN_VERSION}-alpine AS builder
10+
WORKDIR /app
11+
ENV NEXT_TELEMETRY_DISABLED=1
12+
COPY --from=deps /app/node_modules ./node_modules
13+
COPY . .
14+
RUN bun run build
15+
16+
FROM oven/bun:${BUN_VERSION}-alpine AS runner
17+
WORKDIR /app
18+
ENV NODE_ENV=production \
19+
NEXT_TELEMETRY_DISABLED=1 \
20+
PORT=3000 \
21+
PROCESS=webapp
22+
23+
RUN apk add --no-cache nodejs ca-certificates tini \
24+
&& addgroup -S app && adduser -S -G app app
25+
26+
COPY --from=builder --chown=app:app /app/.next/standalone ./
27+
COPY --from=builder --chown=app:app /app/.next/static ./.next/static
28+
COPY --from=builder --chown=app:app /app/public ./public
29+
30+
COPY --from=builder --chown=app:app /app/bridge ./bridge
31+
COPY --from=builder --chown=app:app /app/lib ./lib
32+
COPY --from=builder --chown=app:app /app/db ./db
33+
COPY --from=builder --chown=app:app /app/scripts ./scripts
34+
COPY --from=builder --chown=app:app /app/node_modules ./bridge-node_modules
35+
COPY --from=builder --chown=app:app /app/tsconfig.json ./tsconfig.json
36+
COPY --from=builder --chown=app:app /app/package.json ./package.json
37+
38+
RUN ln -s /app/bridge-node_modules /app/node_modules || true
39+
40+
USER app
41+
EXPOSE 3000
42+
ENTRYPOINT ["/sbin/tini", "--"]
43+
CMD ["bun", "scripts/entrypoint.ts"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Photon
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Codex on iMessage
2+
3+
A coding agent that helps you build and ship with AI — powered by ChatGPT, now on iMessage.
4+
5+
## How it works
6+
7+
1. Paste your OpenAI API key
8+
2. Connect Spectrum and get a phone number
9+
3. Open iMessage with the number and start chatting
10+
11+
Codex replies in the same thread. Send `/new` any time to start a fresh conversation.
12+
13+
## Commands
14+
15+
| Message | What it does |
16+
| ------------------------ | ------------------------------------------------------- |
17+
| _anything_ | Asks Codex. Stateful — follow-ups remember the context. |
18+
| `/new` | Resets the conversation. Bot reacts with 👌 to confirm. |
19+
20+
## Privacy
21+
22+
- Your OpenAI API key is AES-256-GCM encrypted at rest. Plaintext only exists in memory
23+
while a message is being answered.
24+
- Messages go directly to OpenAI's Responses API — nothing is shared with third parties.
25+
- Revoke your key on the OpenAI dashboard any time; the bot will stop replying until you
26+
rotate it.
27+
28+
## License
29+
30+
MIT.

app/api/oauth/device/poll/route.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { pollDeviceToken } from "@/lib/spectrum";
2+
import { cookies } from "next/headers";
3+
import { NextResponse } from "next/server";
4+
5+
export const runtime = "nodejs";
6+
export const dynamic = "force-dynamic";
7+
8+
export async function POST() {
9+
const jar = await cookies();
10+
const deviceCode = jar.get("device_code")?.value;
11+
if (!deviceCode) {
12+
return NextResponse.json(
13+
{ status: "error", reason: "no device flow in progress" },
14+
{ status: 400 },
15+
);
16+
}
17+
18+
let result: Awaited<ReturnType<typeof pollDeviceToken>>;
19+
try {
20+
result = await pollDeviceToken(deviceCode);
21+
} catch (err) {
22+
console.error("[oauth/device/poll] failed:", err);
23+
const host = process.env.SPECTRUM_API_HOST ?? "<unset>";
24+
return NextResponse.json(
25+
{ status: "error", reason: `Couldn't reach Spectrum at ${host}` },
26+
{ status: 502 },
27+
);
28+
}
29+
30+
if (result.ok) {
31+
jar.set("bearer", result.token.access_token, {
32+
httpOnly: true,
33+
sameSite: "lax",
34+
secure: process.env.NODE_ENV === "production",
35+
path: "/",
36+
maxAge: result.token.expires_in ?? 60 * 60 * 24 * 7,
37+
});
38+
jar.delete("device_code");
39+
return NextResponse.json({ status: "ok" });
40+
}
41+
42+
switch (result.error) {
43+
case "authorization_pending":
44+
return NextResponse.json({ status: "pending" });
45+
case "slow_down":
46+
return NextResponse.json({ status: "slow_down" });
47+
case "access_denied":
48+
jar.delete("device_code");
49+
return NextResponse.json({ status: "denied" });
50+
case "expired_token":
51+
jar.delete("device_code");
52+
return NextResponse.json({ status: "expired" });
53+
default:
54+
return NextResponse.json({ status: "error", reason: result.error });
55+
}
56+
}

0 commit comments

Comments
 (0)