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
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,23 @@
- Do not reframe ADE as a docs site, Mintlify project, or generic template app.
- Do not store secrets in plaintext project files when an encrypted store already exists.
- Do not leave policy enforcement in prompts alone when a code path can enforce it directly.

## Cursor Cloud specific instructions

### Environment overview

- **Node.js 22.x** is required (`node:sqlite` is used as the primary database engine).
- Each app under `apps/` has its own independent `node_modules` and `package-lock.json` (no npm workspaces).
- Validation commands are documented in the "Validation" section above.
- The desktop test suite (265 test files) is large; CI shards it. For local iteration, run targeted tests (e.g. `npm --prefix apps/desktop run test:unit`) or a single file rather than the full suite.

### Running the Electron desktop app on Linux

- Set `ADE_DISABLE_HARDWARE_ACCEL=1` — the VM has no real GPU, and without this the app crashes on `WebGL1 blocklisted`.
- `node-pty` ships only macOS/Windows prebuilds. After `npm install`, run `npm --prefix apps/desktop run rebuild:native` to compile `pty.node` for Electron on Linux. Then manually compile the spawn-helper: `cd apps/desktop/node_modules/node-pty && g++ -o build/Release/spawn-helper src/unix/spawn-helper.cc`.
- The `npm run dev` script has a race condition: `predev` clears `dist/`, then tsup + Electron start in parallel, so the first Electron launch fails with "Cannot find module main.cjs" and auto-restarts. To avoid this, pre-build first (`npm run build`) then run the dev launcher directly: `node scripts/normalize-runtime-binaries.cjs && node scripts/ensure-electron.cjs && node scripts/dev.cjs`.
- Alternatively, start Vite and Electron separately for more control: `npx vite --port 5173 --strictPort --force &` then `VITE_DEV_SERVER_URL=http://localhost:5173 npx electron . --no-sandbox`.
- `cr-sqlite` extension binaries are only available for macOS. On Linux the app logs `db.crsqlite_unavailable` as a warning and continues without CRDT sync — this is non-blocking for development.
- The `ADE_PROJECT_ROOT=/workspace` env var tells the main process to auto-open a project at startup. However, there is a timing race: the renderer's initial `getProject()` call may return null before the async project switch completes, causing the welcome screen to appear even though the backend loaded the project. A workaround is to open the project manually via the "Open a project" button in the top bar.
- Computer-use features (screenshot, video capture, GUI automation) are macOS-only (`screencapture`, `osascript`). On Linux these gracefully degrade — the app returns `blocked_by_capability`.
- `electron-builder` config only defines a `mac` target. Distributable Linux builds (deb/AppImage) are not configured, but dev mode works fine.
18 changes: 15 additions & 3 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { app, BrowserWindow, nativeImage, protocol, shell } from "electron";
import path from "node:path";
type NodePtyType = typeof import("node-pty");

Check warning on line 3 in apps/desktop/src/main/main.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden

Check warning on line 3 in apps/desktop/src/main/main.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
import { registerIpc } from "./services/ipc/registerIpc";
import { createFileLogger } from "./services/logging/logger";
import { openKvDb } from "./services/state/kvDb";
Expand Down Expand Up @@ -650,9 +650,10 @@
recordRecent?: boolean;
userSelectedProject?: boolean;
}): Promise<AppContext> => {
// Any pre-existing .ade directory, whether from the tracked shared scaffold or
// a prior local open, means this repo should not feel like a brand-new ADE bootstrap.
const hadAdeDir = fs.existsSync(path.join(projectRoot, ".ade"));
// The .ade directory may exist from git (shared scaffold files like ade.yaml),
// but the db is gitignored and machine-local. A missing db means this machine
// has never completed setup, so onboarding should run.
const hadAdeDir = fs.existsSync(path.join(projectRoot, ".ade", "ade.db"));
const adePaths = ensureAdeDirs(projectRoot);
const { initApiKeyStore } = await import("./services/ai/apiKeyStore");
initApiKeyStore(projectRoot);
Expand Down Expand Up @@ -860,6 +861,17 @@
projectConfigService
});

if (!hadAdeDir) {
const hasEnvCredentials =
Boolean((process.env.GITHUB_TOKEN ?? process.env.ADE_GITHUB_TOKEN ?? "").trim()) ||
Boolean((process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? "").trim()) ||
Boolean((process.env.LINEAR_API_KEY ?? process.env.ADE_LINEAR_TOKEN ?? "").trim());
if (hasEnvCredentials) {
onboardingService.complete();
logger.info("onboarding.auto_completed", { reason: "env_credentials_detected" });
}
}

rebaseSuggestionService = createRebaseSuggestionService({
db,
logger,
Expand Down
21 changes: 20 additions & 1 deletion apps/desktop/src/main/services/ai/apiKeyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,30 @@ export function storeApiKey(provider: string, key: string): void {
persist();
}

const ENV_KEY_PROVIDERS: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY",
openai: "OPENAI_API_KEY",
google: "GOOGLE_API_KEY",
mistral: "MISTRAL_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
xai: "XAI_API_KEY",
groq: "GROQ_API_KEY",
together: "TOGETHER_API_KEY",
openrouter: "OPENROUTER_API_KEY",
};

export function getApiKey(provider: string): string | null {
const normalizedProvider = provider.trim().toLowerCase();
if (!normalizedProvider.length) return null;
const store = ensureStore();
return store[normalizedProvider] ?? null;
const stored = store[normalizedProvider];
if (stored) return stored;
const envVar = ENV_KEY_PROVIDERS[normalizedProvider];
if (envVar) {
const envValue = (process.env[envVar] ?? "").trim();
if (envValue.length > 0) return envValue;
}
return null;
}

export function deleteApiKey(provider: string): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ export function createLinearCredentialService(args: LinearCredentialServiceArgs)
if (cachedToken !== undefined) return cachedToken;
importLegacyTokenIfNeeded();
cachedToken = readEncryptedToken();
if (!cachedToken) {
const envToken = (process.env.LINEAR_API_KEY ?? process.env.ADE_LINEAR_TOKEN ?? "").trim();
if (envToken.length > 0) {
cachedToken = { token: envToken, authMode: "manual" };
}
}
return cachedToken;
};

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/main/services/github/githubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ export function createGithubService({

const readStoredToken = (): string | null => {
const token = migrateLegacyTokenIfNeeded();
if (!token) {
tokenDecryptionFailed = false;
}
return token;
if (token) return token;
tokenDecryptionFailed = false;
const envToken = (process.env.GITHUB_TOKEN ?? process.env.ADE_GITHUB_TOKEN ?? "").trim();
return envToken.length > 0 ? envToken : null;
};

const persistToken = (token: string | null): void => {
Expand Down
8 changes: 3 additions & 5 deletions apps/desktop/src/main/services/state/kvDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3129,17 +3129,15 @@ export async function openKvDb(dbPath: string, logger: Logger): Promise<AdeDb> {
return getRow<T>(db, sql, params);
};

const crsqliteUnavailableError = () => new Error("cr-sqlite extension not available on this platform");

const sync: AdeDbSyncApi = {
getSiteId: () => desiredSiteId,
getDbVersion: () => {
if (!hasCrsqlite) throw crsqliteUnavailableError();
if (!hasCrsqlite) return 0;
const row = get<{ db_version: number }>("select crsql_db_version() as db_version");
return Number(row?.db_version ?? 0);
},
exportChangesSince: (version: number) => {
if (!hasCrsqlite) throw crsqliteUnavailableError();
if (!hasCrsqlite) return [];
const rows = allRows<{
table_name: string;
pk: unknown;
Expand Down Expand Up @@ -3180,7 +3178,7 @@ export async function openKvDb(dbPath: string, logger: Logger): Promise<AdeDb> {
}));
},
applyChanges: (changes: CrsqlChangeRow[]) => {
if (!hasCrsqlite) throw crsqliteUnavailableError();
if (!hasCrsqlite) return { appliedCount: 0, dbVersion: 0, touchedTables: [], rebuiltFts: false };
let appliedCount = 0;
const touchedTables = new Set<string>();
runStatement(db, "begin");
Expand Down
Loading