Skip to content
Closed
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
12 changes: 0 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,6 @@ POSTHOG_PROJECT_ID=
# -----------------------------------------------------------------------------
FREESTYLE_API_KEY=

# -----------------------------------------------------------------------------
# Streams (AI Chat Server)
# -----------------------------------------------------------------------------
# Clients (Desktop Web Mobile)
NEXT_PUBLIC_STREAMS_URL=http://localhost:8080

# Streams server internals (optional)
STREAMS_PORT=8080
STREAMS_INTERNAL_PORT=8081
STREAMS_INTERNAL_URL=http://localhost:8081
STREAMS_DATA_DIR=./data

# -----------------------------------------------------------------------------
# Sentry Error Tracking
# -----------------------------------------------------------------------------
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ jobs:
NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }}
NEXT_PUBLIC_STREAMS_URL: ${{ secrets.NEXT_PUBLIC_STREAMS_URL }}
SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SUPERSET_WORKSPACE_NAME: superset
Expand Down Expand Up @@ -195,7 +194,6 @@ jobs:
NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }}
NEXT_PUBLIC_STREAMS_URL: ${{ secrets.NEXT_PUBLIC_STREAMS_URL }}
NEXT_PUBLIC_ELECTRIC_URL: ${{ secrets.NEXT_PUBLIC_ELECTRIC_URL }}
SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
Expand Down
13 changes: 0 additions & 13 deletions .superset/lib/setup/steps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,6 @@ step_write_env() {
local DOCS_PORT=$((BASE + 4))
local DESKTOP_VITE_PORT=$((BASE + 5))
local DESKTOP_NOTIFICATIONS_PORT=$((BASE + 6))
local STREAMS_PORT=$((BASE + 7))
local STREAMS_INTERNAL_PORT=$((BASE + 8))
local ELECTRIC_PORT=$((BASE + 9))
local CADDY_ELECTRIC_PORT=$((BASE + 10))
local CODE_INSPECTOR_PORT=$((BASE + 11))
Expand All @@ -379,8 +377,6 @@ step_write_env() {
write_env_var "DOCS_PORT" "$DOCS_PORT"
write_env_var "DESKTOP_VITE_PORT" "$DESKTOP_VITE_PORT"
write_env_var "DESKTOP_NOTIFICATIONS_PORT" "$DESKTOP_NOTIFICATIONS_PORT"
write_env_var "STREAMS_PORT" "$STREAMS_PORT"
write_env_var "STREAMS_INTERNAL_PORT" "$STREAMS_INTERNAL_PORT"
write_env_var "ELECTRIC_PORT" "$ELECTRIC_PORT"
write_env_var "CADDY_ELECTRIC_PORT" "$CADDY_ELECTRIC_PORT"
write_env_var "CODE_INSPECTOR_PORT" "$CODE_INSPECTOR_PORT"
Expand All @@ -396,13 +392,6 @@ step_write_env() {
write_env_var "EXPO_PUBLIC_WEB_URL" "http://localhost:$WEB_PORT"
write_env_var "EXPO_PUBLIC_API_URL" "http://localhost:$API_PORT"
echo ""
echo "# Streams URLs (overrides from root .env)"
write_env_var "PORT" "$STREAMS_PORT"
write_env_var "STREAMS_URL" "http://localhost:$STREAMS_PORT"
write_env_var "NEXT_PUBLIC_STREAMS_URL" "http://localhost:$STREAMS_PORT"
write_env_var "EXPO_PUBLIC_STREAMS_URL" "http://localhost:$STREAMS_PORT"
write_env_var "STREAMS_INTERNAL_URL" "http://127.0.0.1:$STREAMS_INTERNAL_PORT"
echo ""
echo "# Electric URLs (overrides from root .env)"
write_env_var "ELECTRIC_URL" "http://localhost:$ELECTRIC_PORT/v1/shape"
echo "# Caddy HTTPS proxy for HTTP/2 (avoids browser 6-connection limit with 10+ SSE streams)"
Expand Down Expand Up @@ -435,8 +424,6 @@ step_write_env() {
{ "port": $DOCS_PORT, "label": "Docs" },
{ "port": $DESKTOP_VITE_PORT, "label": "Desktop Vite" },
{ "port": $DESKTOP_NOTIFICATIONS_PORT, "label": "Notifications" },
{ "port": $STREAMS_PORT, "label": "Streams" },
{ "port": $STREAMS_INTERNAL_PORT, "label": "Streams Internal" },
{ "port": $ELECTRIC_PORT, "label": "Electric" },
{ "port": $CADDY_ELECTRIC_PORT, "label": "Caddy Electric" },
{ "port": $CODE_INSPECTOR_PORT, "label": "Code Inspector" }
Expand Down
2 changes: 0 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Bun + Turbo monorepo with:
- `apps/desktop` - Electron desktop application
- `apps/docs` - Documentation site
- `apps/mobile` - React Native mobile app (Expo)
- `apps/streams` - Streams service
- `apps/electric-proxy` - Electric proxy service
- **Packages**:
- `packages/ui` - Shared UI components (shadcn/ui + TailwindCSS v4).
Expand All @@ -26,7 +25,6 @@ Bun + Turbo monorepo with:
- `packages/mcp` - MCP integration
- `packages/desktop-mcp` - Desktop MCP server
- `packages/local-db` - Local SQLite database
- `packages/durable-session` - Durable session management
- `packages/email` - Email templates/sending
- `packages/scripts` - CLI tooling
- **Tooling**:
Expand Down
12 changes: 0 additions & 12 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ export default defineConfig({
process.env.NEXT_PUBLIC_API_URL,
"https://api.superset.sh",
),
"process.env.NEXT_PUBLIC_STREAMS_URL": defineEnv(
process.env.NEXT_PUBLIC_STREAMS_URL,
"https://streams.superset.sh",
),
"process.env.NEXT_PUBLIC_WEB_URL": defineEnv(
process.env.NEXT_PUBLIC_WEB_URL,
"https://app.superset.sh",
Expand All @@ -75,10 +71,6 @@ export default defineConfig({
"process.env.NEXT_PUBLIC_POSTHOG_HOST": defineEnv(
process.env.NEXT_PUBLIC_POSTHOG_HOST,
),
"process.env.STREAMS_URL": defineEnv(
process.env.STREAMS_URL,
"https://superset-stream.fly.dev",
),
"process.env.DESKTOP_VITE_PORT": defineEnv(process.env.DESKTOP_VITE_PORT),
"process.env.DESKTOP_NOTIFICATIONS_PORT": defineEnv(
process.env.DESKTOP_NOTIFICATIONS_PORT,
Expand Down Expand Up @@ -176,10 +168,6 @@ export default defineConfig({
"import.meta.env.SENTRY_DSN_DESKTOP": defineEnv(
process.env.SENTRY_DSN_DESKTOP,
),
"process.env.STREAMS_URL": defineEnv(
process.env.STREAMS_URL,
"https://superset-stream.fly.dev",
),
"process.env.DESKTOP_VITE_PORT": defineEnv(process.env.DESKTOP_VITE_PORT),
"process.env.DESKTOP_NOTIFICATIONS_PORT": defineEnv(
process.env.DESKTOP_NOTIFICATIONS_PORT,
Expand Down
3 changes: 1 addition & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@durable-streams/client": "^0.2.1",
"@electric-sql/client": "1.5.2",
"@headless-tree/core": "^1.6.3",
"@headless-tree/react": "^1.6.3",
Expand All @@ -53,12 +52,12 @@
"@superset/auth": "workspace:*",
"@superset/db": "workspace:*",
"@superset/desktop-mcp": "workspace:*",
"@superset/durable-session": "workspace:*",
"@superset/local-db": "workspace:*",
"@superset/shared": "workspace:*",
"@superset/trpc": "workspace:*",
"@superset/ui": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"@tanstack/ai": "^0.3.0",
"@tanstack/db": "0.5.25",
"@tanstack/electric-db-collection": "0.2.31",
"@tanstack/react-db": "0.1.69",
Expand Down
66 changes: 31 additions & 35 deletions apps/desktop/src/lib/trpc/routers/ai-chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import {
toAISdkV5Messages,
} from "@superset/agent";
import { observable } from "@trpc/server/observable";
import { env } from "main/env.main";
import { z } from "zod";
import { publicProcedure, router } from "../..";
import { loadToken } from "../auth/utils/auth-functions";
import {
getCredentialsFromConfig,
getCredentialsFromKeychain,
getExistingClaudeCredentials,
} from "./utils/auth/auth";
import {
readClaudeSessionMessages,
Expand Down Expand Up @@ -286,14 +285,40 @@ function scanCustomCommands(cwd: string): CommandEntry[] {

export const createAiChatRouter = () => {
return router({
getConfig: publicProcedure.query(async () => {
const { token } = await loadToken();
getAuthStatus: publicProcedure.query(() => {
const creds = getExistingClaudeCredentials();
if (creds) {
return {
authenticated: true as const,
source: creds.source,
kind: creds.kind,
};
}
return {
proxyUrl: env.NEXT_PUBLIC_STREAMS_URL,
authToken: token,
authenticated: false as const,
source: null,
kind: null,
};
}),
Comment on lines +288 to 302
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

getAuthStatus will return false even after a successful setApiKey call — auth prompt will never dismiss.

getAuthStatus calls getExistingClaudeCredentials(), which only reads from config files and the macOS Keychain. However, setApiKey (Lines 304-320) only stores the key in-memory via setAnthropicAuthToken. After the client invalidates getAuthStatus, the re-query still finds no persisted credentials, so authenticated remains false and the AuthPrompt reappears.

Either getAuthStatus needs to also check the in-memory token state, or setApiKey needs to persist the credential to a config file / keychain so that getExistingClaudeCredentials can find it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 288 - 302,
getAuthStatus currently only checks getExistingClaudeCredentials
(config/keychain) so it stays false after setApiKey stores the token only
in-memory via setAnthropicAuthToken; update getAuthStatus to also inspect the
in-memory token state (call or import the same accessor that exposes the
in-memory token used by setAnthropicAuthToken) and return authenticated:true
when that token exists, or alternatively modify setApiKey to persist the
credential the same way getExistingClaudeCredentials expects (config/keychain)
so that getExistingClaudeCredentials will return a credential; reference
getAuthStatus, getExistingClaudeCredentials, setApiKey, setAnthropicAuthToken
and AuthPrompt when making the change.


setApiKey: publicProcedure
.input(z.object({ apiKey: z.string().min(1) }))
.mutation(({ input }) => {
const isOauth = input.apiKey.startsWith("sk-ant-oat");
if (isOauth) {
setAnthropicAuthToken(input.apiKey);
console.log(
"[ai-chat/setApiKey] Set OAuth token via setAnthropicAuthToken",
);
} else {
setAnthropicAuthToken(input.apiKey);
console.log(
"[ai-chat/setApiKey] Set API key via setAnthropicAuthToken",
);
}
return { success: true };
}),

getModels: publicProcedure.query(() => getAvailableModels()),

getMessages: publicProcedure
Expand Down Expand Up @@ -359,15 +384,6 @@ export const createAiChatRouter = () => {
return { success: true };
}),

interrupt: publicProcedure
.input(z.object({ sessionId: z.string() }))
.mutation(async ({ input }) => {
await chatSessionManager.interrupt({
sessionId: input.sessionId,
});
return { success: true };
}),

stopSession: publicProcedure
.input(z.object({ sessionId: z.string() }))
.mutation(async ({ input }) => {
Expand Down Expand Up @@ -694,25 +710,5 @@ export const createAiChatRouter = () => {
});
return { success: true };
}),

/** Legacy: approve tool use via session manager */
approveToolUse: publicProcedure
.input(
z.object({
sessionId: z.string(),
toolUseId: z.string(),
approved: z.boolean(),
updatedInput: z.record(z.string(), z.unknown()).optional(),
}),
)
.mutation(({ input }) => {
chatSessionManager.resolvePermission({
sessionId: input.sessionId,
toolUseId: input.toolUseId,
approved: input.approved,
updatedInput: input.updatedInput,
});
return { success: true };
}),
});
};
35 changes: 33 additions & 2 deletions apps/desktop/src/lib/trpc/routers/ai-chat/utils/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,40 @@ export function getCredentialsFromKeychain(): ClaudeCredentials | null {
return null;
}

// Claude Code stores OAuth credentials under "Claude Code-credentials"
// as a JSON blob with the same shape as ClaudeConfigFile.
try {
const raw = execSync(
'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
{ encoding: "utf-8" },
).trim();

if (raw) {
try {
const config: ClaudeConfigFile = JSON.parse(raw);
if (config.claudeAiOauth?.accessToken) {
console.log(
"[claude/auth] Found OAuth credentials in macOS Keychain (Claude Code-credentials)",
);
return {
apiKey: config.claudeAiOauth.accessToken,
source: "keychain",
kind: "oauth",
};
}
} catch {
// Not valid JSON — treat as raw API key
console.log(
"[claude/auth] Found raw credentials in macOS Keychain (Claude Code-credentials)",
);
return { apiKey: raw, source: "keychain", kind: "apiKey" };
}
Comment on lines +116 to +135
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Valid JSON without claudeAiOauth.accessToken is silently ignored.

If the keychain entry parses as JSON but lacks claudeAiOauth?.accessToken, execution falls through without returning a credential or logging a warning. Other valid fields (e.g., apiKey, oauth_access_token) present in the JSON would be skipped, unlike the config-file path which checks multiple fields. If the JSON shape varies, consider checking additional fields like getCredentialsFromConfig does, or at minimum log a warning.

Proposed fix
 			try {
 				const config: ClaudeConfigFile = JSON.parse(raw);
 				if (config.claudeAiOauth?.accessToken) {
 					console.log(
 						"[claude/auth] Found OAuth credentials in macOS Keychain (Claude Code-credentials)",
 					);
 					return {
 						apiKey: config.claudeAiOauth.accessToken,
 						source: "keychain",
 						kind: "oauth",
 					};
 				}
+				// Check other known fields in the JSON
+				const apiKey = config.apiKey || config.api_key;
+				const oauthToken = config.oauthAccessToken || config.oauth_access_token;
+				if (apiKey) {
+					console.log("[claude/auth] Found API key in macOS Keychain (Claude Code-credentials)");
+					return { apiKey, source: "keychain", kind: "apiKey" };
+				}
+				if (oauthToken) {
+					console.log("[claude/auth] Found OAuth token in macOS Keychain (Claude Code-credentials)");
+					return { apiKey: oauthToken, source: "keychain", kind: "oauth" };
+				}
+				console.warn("[claude/auth] Claude Code-credentials JSON found but no recognized credential fields");
 			} catch {
 				// Not valid JSON — treat as raw API key
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (raw) {
try {
const config: ClaudeConfigFile = JSON.parse(raw);
if (config.claudeAiOauth?.accessToken) {
console.log(
"[claude/auth] Found OAuth credentials in macOS Keychain (Claude Code-credentials)",
);
return {
apiKey: config.claudeAiOauth.accessToken,
source: "keychain",
kind: "oauth",
};
}
} catch {
// Not valid JSON — treat as raw API key
console.log(
"[claude/auth] Found raw credentials in macOS Keychain (Claude Code-credentials)",
);
return { apiKey: raw, source: "keychain", kind: "apiKey" };
}
if (raw) {
try {
const config: ClaudeConfigFile = JSON.parse(raw);
if (config.claudeAiOauth?.accessToken) {
console.log(
"[claude/auth] Found OAuth credentials in macOS Keychain (Claude Code-credentials)",
);
return {
apiKey: config.claudeAiOauth.accessToken,
source: "keychain",
kind: "oauth",
};
}
// Check other known fields in the JSON
const apiKey = config.apiKey || config.api_key;
const oauthToken = config.oauthAccessToken || config.oauth_access_token;
if (apiKey) {
console.log("[claude/auth] Found API key in macOS Keychain (Claude Code-credentials)");
return { apiKey, source: "keychain", kind: "apiKey" };
}
if (oauthToken) {
console.log("[claude/auth] Found OAuth token in macOS Keychain (Claude Code-credentials)");
return { apiKey: oauthToken, source: "keychain", kind: "oauth" };
}
console.warn("[claude/auth] Claude Code-credentials JSON found but no recognized credential fields");
} catch {
// Not valid JSON — treat as raw API key
console.log(
"[claude/auth] Found raw credentials in macOS Keychain (Claude Code-credentials)",
);
return { apiKey: raw, source: "keychain", kind: "apiKey" };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/ai-chat/utils/auth/auth.ts` around lines
116 - 135, The code path that parses raw macOS Keychain JSON (ClaudeConfigFile)
silently ignores valid JSON that lacks claudeAiOauth.accessToken; update the
logic in the auth parsing block (the JSON.parse branch in auth.ts) to reuse the
same extraction logic used by getCredentialsFromConfig (or explicitly check for
other fields such as apiKey and oauth_access_token) and return the appropriate
credential object (kind/source) when found, and if no known fields are present
emit a clear warning log mentioning the parsed JSON keys and that no usable
credential was found (instead of falling through silently); reference
ClaudeConfigFile, claudeAiOauth, and getCredentialsFromConfig to locate where to
apply this change.

}
} catch {
// Not found in keychain, this is fine
}

try {
// Claude CLI stores credentials in the keychain with this service/account
const result = execSync(
'security find-generic-password -s "claude-cli" -a "api-key" -w 2>/dev/null',
{ encoding: "utf-8" },
Expand All @@ -120,7 +152,6 @@ export function getCredentialsFromKeychain(): ClaudeCredentials | null {
// Not found in keychain, this is fine
}

// Try alternate keychain entry format
try {
const result = execSync(
'security find-generic-password -s "anthropic-api-key" -w 2>/dev/null',
Expand Down
Loading
Loading