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
16 changes: 12 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,18 @@ POSTHOG_PROJECT_ID=
FREESTYLE_API_KEY=

# -----------------------------------------------------------------------------
# Durable Streams
# -----------------------------------------------------------------------------
DURABLE_STREAM_URL=
DURABLE_STREAM_AUTH_TOKEN=
# Streams (AI Chat Server)
# -----------------------------------------------------------------------------
# Desktop app / client-facing
STREAMS_URL=http://localhost:8080
STREAMS_SECRET=

# Streams server internals
PORT=8080
STREAMS_INTERNAL_PORT=8081
STREAMS_AGENT_PORT=9090
STREAMS_INTERNAL_URL=http://127.0.0.1:8081
STREAMS_DATA_DIR=.data

# -----------------------------------------------------------------------------
# Sentry Error Tracking
Expand Down
5 changes: 5 additions & 0 deletions .github/templates/preview-comment.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
<td>$ELECTRIC_LINK</td>
</tr>
<tr>
<td><img src="https://fly.io/phx/ui/images/favicon/favicon-595d1312b35dfe32838befdf8505515e.ico" width="20" height="20" alt="Fly.io"> <strong>Streams (Fly.io)</strong></td>
<td align="center">$STREAMS_STATUS</td>
<td>$STREAMS_LINK</td>
</tr>
<tr>
<td><img src="https://vercel.com/favicon.ico" width="20" height="20" alt="Vercel"> <strong>API (Vercel)</strong></td>
<td align="center">$API_STATUS</td>
<td>$API_LINK</td>
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/cleanup-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ jobs:
run: |
flyctl apps destroy "superset-electric-pr-${{ github.event.pull_request.number }}" --yes

- name: Delete Streams Fly.io app
id: streams-cleanup
continue-on-error: true
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
flyctl apps destroy "superset-stream-pr-${{ github.event.pull_request.number }}" --yes

- name: Update comment
if: always()
uses: thollander/actions-comment-pull-request@v3
Expand All @@ -43,6 +51,7 @@ jobs:
The following preview resources have been cleaned up:
- ${{ steps.neon-cleanup.outcome == 'success' && '✅' || '⚠️' }} Neon database branch
- ${{ steps.electric-cleanup.outcome == 'success' && '✅' || '⚠️' }} Electric Fly.io app
- ${{ steps.streams-cleanup.outcome == 'success' && '✅' || '⚠️' }} Streams Fly.io app

Thank you for your contribution! 🎉
comment-tag: "🚀-preview-deployment"
41 changes: 39 additions & 2 deletions .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ env:
ADMIN_ALIAS: admin-pr-${{ github.event.pull_request.number }}-superset.vercel.app
DOCS_ALIAS: docs-pr-${{ github.event.pull_request.number }}-superset.vercel.app
ELECTRIC_URL: https://superset-electric-pr-${{ github.event.pull_request.number }}.fly.dev/v1/shape
STREAMS_URL: https://superset-stream-pr-${{ github.event.pull_request.number }}.fly.dev

jobs:
deploy-database:
Expand Down Expand Up @@ -120,6 +121,36 @@ jobs:
name: electric-status
path: electric-status.env

deploy-streams-preview:
name: Deploy Streams (Fly.io)
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Deploy Streams preview to Fly.io
uses: superfly/fly-pr-review-apps@1.3.0
with:
name: superset-stream-pr-${{ github.event.pull_request.number }}
region: iad
org: ${{ vars.FLY_ORG }}
config: apps/streams/fly.toml
secrets: |
ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}
STREAMS_SECRET=${{ secrets.STREAMS_SECRET }}
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Save streams status
run: |
cat > streams-status.env << EOF
STREAMS_STATUS="✅"
STREAMS_LINK="<a href=\"https://fly.io/apps/superset-stream-pr-${{ github.event.pull_request.number }}\">View App</a>"
EOF
- name: Upload streams status
uses: actions/upload-artifact@v4
with:
name: streams-status
path: streams-status.env

deploy-api:
name: Deploy API
runs-on: ubuntu-latest
Expand Down Expand Up @@ -649,7 +680,7 @@ jobs:
name: Post Deployment Comment
runs-on: ubuntu-latest
if: always()
needs: [deploy-database, deploy-electric, deploy-api, deploy-web, deploy-marketing, deploy-admin, deploy-docs]
needs: [deploy-database, deploy-electric, deploy-streams-preview, deploy-api, deploy-web, deploy-marketing, deploy-admin, deploy-docs]
permissions:
contents: read
pull-requests: write
Expand All @@ -670,6 +701,8 @@ jobs:
DATABASE_LINK="Failed to create"
ELECTRIC_STATUS="❌"
ELECTRIC_LINK="Failed to deploy"
STREAMS_STATUS="❌"
STREAMS_LINK="Failed to deploy"
API_STATUS="❌"
API_LINK="Failed to deploy"
WEB_STATUS="❌"
Expand All @@ -689,6 +722,10 @@ jobs:
source electric-status.env
fi

if [[ "${{ needs.deploy-streams-preview.result }}" == "success" ]]; then
source streams-status.env
fi

if [[ "${{ needs.deploy-api.result }}" == "success" ]]; then
source api-status.env
fi
Expand All @@ -709,7 +746,7 @@ jobs:
source docs-status.env
fi

export DATABASE_STATUS DATABASE_LINK ELECTRIC_STATUS ELECTRIC_LINK API_STATUS API_LINK WEB_STATUS WEB_LINK MARKETING_STATUS MARKETING_LINK ADMIN_STATUS ADMIN_LINK DOCS_STATUS DOCS_LINK
export DATABASE_STATUS DATABASE_LINK ELECTRIC_STATUS ELECTRIC_LINK STREAMS_STATUS STREAMS_LINK API_STATUS API_LINK WEB_STATUS WEB_LINK MARKETING_STATUS MARKETING_LINK ADMIN_STATUS ADMIN_LINK DOCS_STATUS DOCS_LINK
envsubst < .github/templates/preview-comment.md > final-comment.md

- name: Post final deployment comment
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,28 @@ jobs:
--env STRIPE_PRO_YEARLY_PRICE_ID=$STRIPE_PRO_YEARLY_PRICE_ID \
--env SLACK_BILLING_WEBHOOK_URL=$SLACK_BILLING_WEBHOOK_URL

deploy-streams:
name: Deploy Streams to Fly.io
runs-on: ubuntu-latest
environment: production

steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Stage secrets
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
flyctl secrets set \
ANTHROPIC_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" \
STREAMS_SECRET="${{ secrets.STREAMS_SECRET }}" \
--app superset-stream \
--stage
- name: Deploy to Fly.io
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: flyctl deploy --config apps/streams/fly.toml --remote-only

deploy-docs:
name: Deploy Docs to Vercel
runs-on: ubuntu-latest
Expand Down
21 changes: 20 additions & 1 deletion .superset/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,16 @@ step_start_electric() {
return 0
}

step_setup_streams() {
echo "🔄 Setting up Streams secret..."

STREAMS_SECRET=$(openssl rand -hex 32)
export STREAMS_SECRET

success "Streams secret generated"
return 0
}

step_write_env() {
echo "📝 Writing .env file..."

Expand Down Expand Up @@ -341,6 +351,10 @@ step_write_env() {
if [ -n "${ELECTRIC_SECRET:-}" ]; then
echo "ELECTRIC_SECRET=$ELECTRIC_SECRET"
fi

echo ""
echo "# Workspace Streams (AI Chat Server)"
echo "STREAMS_SECRET=$STREAMS_SECRET"
} >> .env

success "Workspace .env written"
Expand Down Expand Up @@ -376,7 +390,12 @@ main() {
step_failed "Start Electric SQL"
fi

# Step 6: Write .env file
# Step 6: Setup Streams secret
if ! step_setup_streams; then
step_failed "Setup Streams secret"
fi

# Step 7: Write .env file
if ! step_write_env; then
step_failed "Write .env file"
fi
Expand Down
8 changes: 3 additions & 5 deletions apps/desktop/src/lib/trpc/routers/ai-chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { observable } from "@trpc/server/observable";
import { env } from "main/env.main";
import { z } from "zod";
import { publicProcedure, router } from "../..";
import {
Expand Down Expand Up @@ -60,11 +61,8 @@ function scanCustomCommands(cwd: string): CommandEntry[] {
export const createAiChatRouter = () => {
return router({
getConfig: publicProcedure.query(() => ({
proxyUrl: process.env.DURABLE_STREAM_URL || "http://localhost:8080",
authToken:
process.env.DURABLE_STREAM_AUTH_TOKEN ||
process.env.DURABLE_STREAM_TOKEN ||
null,
proxyUrl: env.STREAMS_URL,
authToken: env.STREAMS_SECRET,
})),

getSlashCommands: publicProcedure
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { EventEmitter } from "node:events";
import { env } from "main/env.main";
import type { AgentProvider } from "../agent-provider";
import type { SessionStore } from "../session-store";

const PROXY_URL = process.env.DURABLE_STREAM_URL || "http://localhost:8080";
const DURABLE_STREAM_AUTH_TOKEN =
process.env.DURABLE_STREAM_AUTH_TOKEN || process.env.DURABLE_STREAM_TOKEN;
const PROXY_URL = env.STREAMS_URL;
const STREAMS_SECRET = env.STREAMS_SECRET;

/**
* Set, clear, or skip a field on a body template.
Expand All @@ -26,13 +26,10 @@ function applyBodyField(
}

function buildProxyHeaders(): Record<string, string> {
const headers: Record<string, string> = {
return {
"Content-Type": "application/json",
Authorization: `Bearer ${STREAMS_SECRET}`,
};
if (DURABLE_STREAM_AUTH_TOKEN) {
headers.Authorization = `Bearer ${DURABLE_STREAM_AUTH_TOKEN}`;
}
return headers;
}

export interface SessionStartEvent {
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/env.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const env = createEnv({
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"),
SENTRY_DSN_DESKTOP: z.string().optional(),
STREAMS_URL: z.url(),
STREAMS_SECRET: z.string().min(1),
},

runtimeEnv: {
Expand All @@ -35,6 +37,8 @@ export const env = createEnv({
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
SENTRY_DSN_DESKTOP: process.env.SENTRY_DSN_DESKTOP,
STREAMS_URL: process.env.STREAMS_URL,
STREAMS_SECRET: process.env.STREAMS_SECRET,
},
emptyStringAsUndefined: true,
// Only allow skipping validation in development (never in production)
Expand Down
1 change: 1 addition & 0 deletions apps/streams/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@durable-streams/server": "^0.2.0",
"@hono/node-server": "^1.13.0",
"@superset/durable-session": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"@tanstack/ai": "^0.3.0",
"@tanstack/db": "0.5.22",
"hono": "^4.4.0",
Expand Down
6 changes: 1 addition & 5 deletions apps/streams/src/claude-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,6 @@ app.post("/", async (c) => {
const queryEnv: Record<string, string> = { ...baseEnv };
queryEnv.CLAUDE_CODE_ENTRYPOINT = "sdk-ts";

const binaryPath = process.env.CLAUDE_BINARY_PATH;

const hooks = notification
? buildNotificationHooks({ notification })
: undefined;
Expand Down Expand Up @@ -164,7 +162,7 @@ app.post("/", async (c) => {
options: {
...(claudeSessionId && { resume: claudeSessionId }),
...(cwd && { cwd }),
model: model ?? process.env.CLAUDE_MODEL ?? DEFAULT_MODEL,
model: model ?? DEFAULT_MODEL,
maxTurns: MAX_AGENT_TURNS,
includePartialMessages: true,
permissionMode: resolvedPermissionMode as
Expand All @@ -173,7 +171,6 @@ app.post("/", async (c) => {
| "bypassPermissions",
settingSources: ["user", "project", "local"],
systemPrompt: { type: "preset" as const, preset: "claude_code" as const },
...(binaryPath && { pathToClaudeCodeExecutable: binaryPath }),
env: queryEnv,
abortController,
...(hooks && { hooks }),
Expand Down Expand Up @@ -361,7 +358,6 @@ app.get("/health", (c) => {
return c.json({
status: "ok",
agent: "claude",
hasBinary: !!process.env.CLAUDE_BINARY_PATH,
activeSessions: getActiveSessionCount(),
});
});
Expand Down
6 changes: 2 additions & 4 deletions apps/streams/src/claude-session-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { env } from "./env";

const SESSION_MAX_SIZE = 1000;
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
Expand All @@ -12,9 +12,7 @@ interface SessionEntry {

const claudeSessions = new Map<string, SessionEntry>();

const SESSIONS_DIR =
process.env.DURABLE_STREAMS_DATA_DIR ??
join(homedir(), ".superset", "chat-streams");
const SESSIONS_DIR = env.STREAMS_DATA_DIR;
const SESSIONS_FILE = join(SESSIONS_DIR, "claude-sessions.json");

function loadPersistedSessions(): void {
Expand Down
19 changes: 19 additions & 0 deletions apps/streams/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";

export const env = createEnv({
server: {
PORT: z.coerce.number(),
STREAMS_INTERNAL_PORT: z.coerce.number(),
STREAMS_AGENT_PORT: z.coerce.number(),
STREAMS_INTERNAL_URL: z.string().url(),
STREAMS_DATA_DIR: z.string().min(1),
STREAMS_SECRET: z.string().min(1),
ANTHROPIC_API_KEY: z.string().min(1),
},
clientPrefix: "PUBLIC_",
client: {},
runtimeEnv: process.env,
emptyStringAsUndefined: true,
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
});
Loading