Compare commits

...

7 Commits

Author SHA1 Message Date
L'électron rare ac04f97067 feat: add Mascarade as LLM provider — multi-agent orchestration engine
- Add 'mascarade' to SupportedProvider enum (types.ts)
- Add Mascarade case in createProviderInstance using OpenAI-compatible SDK (factory.ts)
- Add 'mascarade' to BuiltinProvider type (provider-account.ts)
- Add mascarade- prefix to MODEL_PROVIDER_MAP (config/types.ts)
- Add 5 mascarade models: router, writer, coder, analyst, planner (models.ts)
- Add mascarade provider presets for all agent profiles (models.ts)
- Add mascarade to PROVIDER_INFOS with UI metadata (providers.ts)

Mascarade is a self-hosted LLM orchestration engine (50K LOC Python)
that routes requests across Claude, OpenAI, Mistral, Google and Ollama
with intelligent routing strategies (best/cheapest/fastest/domain).
Default endpoint: http://localhost:8100/v1 (OpenAI-compatible)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:24:21 +01:00
AndyMik90 76fdbade6f docs: update README beta download links to 2.8.0-beta.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 20:32:15 +01:00
André Mikalsen 979d97757f fix(deps): update Vercel AI SDK and all dependencies (#1963)
* fix(deps): update Vercel AI SDK and all dependencies to latest

Updates 47 packages including all AI-related dependencies:
- ai (Vercel AI SDK): 6.0.91 → 6.0.116
- @ai-sdk/anthropic: 3.0.45 → 3.0.58
- @ai-sdk/mistral: 2.0.28 → 3.0.24 (major)
- @ai-sdk/google: 3.0.29 → 3.0.43
- @anthropic-ai/sdk: 0.71.2 → 0.78.0
- @modelcontextprotocol/sdk: 1.26.0 → 1.27.1
- zod: 4.2.1 → 4.3.6
- Plus all other @ai-sdk/* providers, UI, tooling deps

Skipped major bumps: electron 40→41, vite 7→8, jsdom 27→29

All 4487 tests pass, typecheck clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(deps): align @electron/rebuild override with devDependency (4.0.2 → 4.0.3)

Addresses PR review feedback from CodeRabbit, Gemini, and Cursor bots.
The override was still pinned to 4.0.2 while devDependency was bumped
to ^4.0.3, creating a version conflict.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:20:56 +01:00
André Mikalsen 3f8e16edb2 fix: skip Claude onboarding for profiles + prevent duplicate accounts (#1952)
* fix: skip Claude Code onboarding for authenticated profiles

When CLAUDE_CONFIG_DIR points to a profile directory, Claude Code reads
.claude.json from that directory instead of ~/.claude.json. Profile
configs created by `claude auth login` don't include hasCompletedOnboarding,
causing the onboarding wizard to appear every time Claude Code is launched.

Set hasCompletedOnboarding: true in the profile's .claude.json after
successful authentication and before each Claude Code invocation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: clean up dead onboarding code paths and add tests

Remove dead `needsOnboarding` UI branch from AuthTerminal.tsx and
stale type declarations from types.ts, ipc.ts, terminal-api.ts.
Remove unreachable `ensureOnboardingComplete` call from
`handleOnboardingComplete` (guard prevents execution). Export
`ensureOnboardingComplete` and add 9 unit tests covering all branches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: allow project-switching shortcuts when terminal is focused

xterm.js uses a hidden <textarea> (xterm-helper-textarea) for keyboard
input. ProjectTabBar's keydown handler skipped all HTMLTextAreaElement
targets, which prevented Cmd/Ctrl+1-9 project switching from working
when a terminal had focus. Exclude xterm's textarea from the skip-filter
since xterm already passes these shortcuts through via
attachCustomKeyEventHandler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use atomic write to satisfy CodeQL insecure-temporary-file rule

Write .claude.json via temp file + rename instead of direct writeFileSync
to address CodeQL js/insecure-temporary-file false positive. The temp file
is created with mode 0o600 (owner-only) and atomically renamed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prevent duplicate provider accounts and clean up dead onboarding API surface

Add backend gate in PROVIDER_ACCOUNTS_SAVE to reject duplicate email+provider
combinations with a user-friendly error. Clean up dead onTerminalOnboardingComplete
IPC surface (preload, types, constants, mock) that was never fired after the
onboarding flow was made proactive. Fix i18n compliance (hardcoded strings in
handleFallbackTerminal, orphaned translation keys) and Codex OAuth silent error
swallowing. Correct vi.mock paths in onboarding test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 11:05:27 +01:00
André Mikalsen 53b55468c9 fix: skip onboarding for profiles + terminal shortcuts (#1949)
* fix: skip Claude Code onboarding for authenticated profiles

When CLAUDE_CONFIG_DIR points to a profile directory, Claude Code reads
.claude.json from that directory instead of ~/.claude.json. Profile
configs created by `claude auth login` don't include hasCompletedOnboarding,
causing the onboarding wizard to appear every time Claude Code is launched.

Set hasCompletedOnboarding: true in the profile's .claude.json after
successful authentication and before each Claude Code invocation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: clean up dead onboarding code paths and add tests

Remove dead `needsOnboarding` UI branch from AuthTerminal.tsx and
stale type declarations from types.ts, ipc.ts, terminal-api.ts.
Remove unreachable `ensureOnboardingComplete` call from
`handleOnboardingComplete` (guard prevents execution). Export
`ensureOnboardingComplete` and add 9 unit tests covering all branches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: allow project-switching shortcuts when terminal is focused

xterm.js uses a hidden <textarea> (xterm-helper-textarea) for keyboard
input. ProjectTabBar's keydown handler skipped all HTMLTextAreaElement
targets, which prevented Cmd/Ctrl+1-9 project switching from working
when a terminal had focus. Exclude xterm's textarea from the skip-filter
since xterm already passes these shortcuts through via
attachCustomKeyEventHandler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use atomic write to satisfy CodeQL insecure-temporary-file rule

Write .claude.json via temp file + rename instead of direct writeFileSync
to address CodeQL js/insecure-temporary-file false positive. The temp file
is created with mode 0o600 (owner-only) and atomically renamed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 09:15:03 +01:00
AndyMik90 1984a62d9b docs: update README beta download links to 2.8.0-beta.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:25:58 +01:00
André Mikalsen a5670a6912 fix(build): bundle @libsql native modules + rebrand to Aperant (#1946)
* fix(build): unpack @libsql/client native modules from asar

@libsql/client has platform-specific native bindings (@libsql/darwin-arm64,
@libsql/linux-x64, etc.) containing .node files that cannot be loaded from
inside app.asar. This causes ERR_MODULE_NOT_FOUND on app startup after
updating to 2.8.0-beta.4.

Add @libsql/client to rollupOptions.external so Vite keeps it as a runtime
require, and add node_modules/@libsql/** to asarUnpack so electron-builder
extracts the native modules to app.asar.unpacked/.

Follows the same pattern used for @lydell/node-pty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix bundled and update name + icon

* fix: complete Aperant rebrand and harden native module loading

- Add try/catch + type validation to loadCreateClient() in db.ts to
  prevent silent failures when @libsql/client native module is missing
  or exports are wrong (was a blocking issue)
- Add path.resolve() and JSON type guard to ensureOnboardingComplete()
  for safer config file handling
- Replace require('fs').cpSync with static import; fix console.log in
  production code (index.ts)
- Complete "Auto Claude" → "Aperant" brand rename across ~30 remaining
  source files: renderer components, GitHub/GitLab PR comment bodies,
  User-Agent headers, MCP registry, and test assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(deps): sync package-lock.json with aperant rename

package.json was renamed from auto-claude-ui to aperant but
package-lock.json wasn't regenerated, causing npm ci to fail
in all CI jobs with "Missing: aperant@2.8.0-beta.1 from lock file".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): resolve CodeQL file-system race in ensureOnboardingComplete

Replace existsSync + readFileSync pattern with direct readFileSync
wrapped in try/catch for ENOENT. Eliminates the TOCTOU race condition
flagged by CodeQL (js/file-system-race).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 21:16:30 +01:00
75 changed files with 1831 additions and 1419 deletions
+7 -7
View File
@@ -36,18 +36,18 @@
> ⚠️ Beta releases may contain bugs and breaking changes. [View all releases](https://github.com/AndyMik90/Auto-Claude/releases)
<!-- BETA_VERSION_BADGE -->
[![Beta](https://img.shields.io/badge/beta-2.8.0--beta.1-orange?style=flat-square)](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.8.0-beta.4)
[![Beta](https://img.shields.io/badge/beta-2.8.0--beta.5-orange?style=flat-square)](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.8.0-beta.5)
<!-- BETA_VERSION_BADGE_END -->
<!-- BETA_DOWNLOADS -->
| Platform | Download |
|----------|----------|
| **Windows** | [Auto-Claude-2.8.0-beta.4-win32-x64.exe](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.4/Auto-Claude-2.8.0-beta.4-win32-x64.exe) |
| **macOS (Apple Silicon)** | [Auto-Claude-2.8.0-beta.4-darwin-arm64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.4/Auto-Claude-2.8.0-beta.4-darwin-arm64.dmg) |
| **macOS (Intel)** | [Auto-Claude-2.8.0-beta.4-darwin-x64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.4/Auto-Claude-2.8.0-beta.4-darwin-x64.dmg) |
| **Linux** | [Auto-Claude-2.8.0-beta.4-linux-x86_64.AppImage](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.4/Auto-Claude-2.8.0-beta.4-linux-x86_64.AppImage) |
| **Linux (Debian)** | [Auto-Claude-2.8.0-beta.4-linux-amd64.deb](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.4/Auto-Claude-2.8.0-beta.4-linux-amd64.deb) |
| **Linux (Flatpak)** | [Auto-Claude-2.8.0-beta.4-linux-x86_64.flatpak](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.4/Auto-Claude-2.8.0-beta.4-linux-x86_64.flatpak) |
| **Windows** | [Aperant-2.8.0-beta.5-win32-x64.exe](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-win32-x64.exe) |
| **macOS (Apple Silicon)** | [Aperant-2.8.0-beta.5-darwin-arm64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-darwin-arm64.dmg) |
| **macOS (Intel)** | [Aperant-2.8.0-beta.5-darwin-x64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-darwin-x64.dmg) |
| **Linux** | [Aperant-2.8.0-beta.5-linux-x86_64.AppImage](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-x86_64.AppImage) |
| **Linux (Debian)** | [Aperant-2.8.0-beta.5-linux-amd64.deb](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-amd64.deb) |
| **Linux (Flatpak)** | [Aperant-2.8.0-beta.5-linux-x86_64.flatpak](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-x86_64.flatpak) |
<!-- BETA_DOWNLOADS_END -->
> All releases include SHA256 checksums and VirusTotal scan results for security verification.
+3 -1
View File
@@ -78,7 +78,9 @@ export default defineConfig({
// spawned via `new Worker(path)` from WorkerBridge
'ai/agent/worker': resolve(__dirname, 'src/main/ai/agent/worker.ts'),
},
// Only node-pty needs to be external (native module rebuilt by electron-builder)
// Native modules that must remain external (loaded from disk, not bundled).
// @libsql/client is loaded lazily via globalThis.require() and resolved
// from extraResources/node_modules via Module.globalPaths (see index.ts).
external: ['@lydell/node-pty']
}
}
+75 -59
View File
@@ -1,16 +1,16 @@
{
"name": "auto-claude-ui",
"name": "aperant",
"version": "2.8.0-beta.1",
"type": "module",
"description": "Desktop UI for Auto Claude autonomous coding framework",
"homepage": "https://github.com/AndyMik90/Auto-Claude",
"description": "Autonomous multi-agent coding framework",
"homepage": "https://github.com/AndyMik90/Aperant",
"repository": {
"type": "git",
"url": "https://github.com/AndyMik90/Auto-Claude.git"
"url": "https://github.com/AndyMik90/Aperant.git"
},
"main": "./out/main/index.js",
"author": {
"name": "Auto Claude Team",
"name": "Aperant Team",
"email": "119136210+AndyMik90@users.noreply.github.com"
},
"license": "AGPL-3.0",
@@ -35,9 +35,9 @@
"package:flatpak": "electron-builder --linux flatpak",
"verify:linux": "node scripts/verify-linux-packages.cjs dist",
"test:verify-linux": "node --test scripts/verify-linux-packages.test.mjs",
"start:packaged:mac": "open dist/mac-arm64/Auto-Claude.app || open dist/mac/Auto-Claude.app",
"start:packaged:win": "start \"\" \"dist\\win-unpacked\\Auto-Claude.exe\"",
"start:packaged:linux": "./dist/linux-unpacked/auto-claude",
"start:packaged:mac": "open dist/mac-arm64/Aperant.app || open dist/mac/Aperant.app",
"start:packaged:win": "start \"\" \"dist\\win-unpacked\\Aperant.exe\"",
"start:packaged:linux": "./dist/linux-unpacked/aperant",
"test": "vitest run",
"test:unit": "vitest run --exclude src/__tests__/integration/ --exclude src/__tests__/e2e/",
"test:integration": "vitest run src/__tests__/integration/",
@@ -50,27 +50,27 @@
"typecheck": "tsc --noEmit --incremental"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.61",
"@ai-sdk/anthropic": "^3.0.45",
"@ai-sdk/azure": "^3.0.31",
"@ai-sdk/google": "^3.0.29",
"@ai-sdk/groq": "^3.0.24",
"@ai-sdk/mcp": "^1.0.21",
"@ai-sdk/mistral": "^2.0.28",
"@ai-sdk/openai": "^3.0.30",
"@ai-sdk/openai-compatible": "^2.0.30",
"@ai-sdk/xai": "^3.0.57",
"@anthropic-ai/sdk": "^0.71.2",
"@ai-sdk/amazon-bedrock": "^4.0.77",
"@ai-sdk/anthropic": "^3.0.58",
"@ai-sdk/azure": "^3.0.42",
"@ai-sdk/google": "^3.0.43",
"@ai-sdk/groq": "^3.0.29",
"@ai-sdk/mcp": "^1.0.25",
"@ai-sdk/mistral": "^3.0.24",
"@ai-sdk/openai": "^3.0.41",
"@ai-sdk/openai-compatible": "^2.0.35",
"@ai-sdk/xai": "^3.0.67",
"@anthropic-ai/sdk": "^0.78.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@libsql/client": "^0.17.0",
"@lydell/node-pty": "^1.1.0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@openrouter/ai-sdk-provider": "^2.2.3",
"@modelcontextprotocol/sdk": "^1.27.1",
"@openrouter/ai-sdk-provider": "^2.3.1",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
@@ -84,89 +84,89 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@sentry/electron": "^7.5.0",
"@sentry/electron": "^7.10.0",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-virtual": "^3.13.13",
"@tanstack/react-virtual": "^3.13.22",
"@tavily/core": "^0.7.2",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-serialize": "^0.14.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0",
"ai": "^6.0.91",
"ai": "^6.0.116",
"chokidar": "^5.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"dotenv": "^17.3.1",
"electron-log": "^5.4.3",
"electron-updater": "^6.6.2",
"i18next": "^25.7.3",
"lucide-react": "^0.562.0",
"minimatch": "^10.1.1",
"motion": "^12.23.26",
"electron-updater": "^6.8.3",
"i18next": "^25.8.18",
"lucide-react": "^0.577.0",
"minimatch": "^10.2.4",
"motion": "^12.36.0",
"proper-lockfile": "^4.1.2",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.8",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"semver": "^7.7.3",
"tailwind-merge": "^3.4.0",
"semver": "^7.7.4",
"tailwind-merge": "^3.5.0",
"uuid": "^13.0.0",
"web-tree-sitter": "^0.26.5",
"xstate": "^5.26.0",
"zod": "^4.2.1",
"zustand": "^5.0.9"
"web-tree-sitter": "^0.26.7",
"xstate": "^5.28.0",
"zod": "^4.3.6",
"zustand": "^5.0.11"
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
"@biomejs/biome": "2.4.7",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@electron/rebuild": "^4.0.2",
"@playwright/test": "^1.52.0",
"@tailwindcss/postcss": "^4.1.17",
"@testing-library/dom": "^10.0.0",
"@electron/rebuild": "^4.0.3",
"@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4.2.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.1.0",
"@testing-library/react": "^16.3.2",
"@types/minimatch": "^6.0.0",
"@types/node": "^25.0.0",
"@types/react": "^19.2.7",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1",
"@types/uuid": "^11.0.0",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.1.0",
"autoprefixer": "^10.4.22",
"autoprefixer": "^10.4.27",
"cross-env": "^10.1.0",
"electron": "40.0.0",
"electron-builder": "^26.4.0",
"electron-builder": "^26.8.1",
"electron-vite": "^5.0.0",
"husky": "^9.1.7",
"jsdom": "^27.3.0",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"lint-staged": "^16.4.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vite": "^7.2.7",
"vitest": "^4.0.16"
"vitest": "^4.1.0"
},
"overrides": {
"electron-builder-squirrel-windows": "^26.0.12",
"dmg-builder": "^26.0.12",
"@electron/rebuild": "4.0.2"
"@electron/rebuild": "4.0.3"
},
"build": {
"appId": "com.autoclaude.ui",
"productName": "Auto-Claude",
"appId": "com.aperant.app",
"productName": "Aperant",
"npmRebuild": false,
"artifactName": "${productName}-${version}-${platform}-${arch}.${ext}",
"publish": [
{
"provider": "github",
"owner": "AndyMik90",
"repo": "Auto-Claude"
"repo": "Aperant"
}
],
"directories": {
@@ -188,6 +188,22 @@
{
"from": "prompts",
"to": "prompts"
},
{
"from": "../../node_modules/@libsql",
"to": "node_modules/@libsql"
},
{
"from": "../../node_modules/libsql",
"to": "node_modules/libsql"
},
{
"from": "../../node_modules/@neon-rs",
"to": "node_modules/@neon-rs"
},
{
"from": "../../node_modules/detect-libc",
"to": "node_modules/detect-libc"
}
],
"mac": {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 B

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -302,7 +302,7 @@ describe('Application Logger', () => {
const { generateDebugReport } = await import('../app-logger');
const report = generateDebugReport();
expect(report).toContain('=== Auto Claude Debug Report ===');
expect(report).toContain('=== Aperant Debug Report ===');
expect(report).toContain('--- System Information ---');
expect(report).toContain('--- Recent Errors ---');
expect(report).toContain('=== End Debug Report ===');
@@ -0,0 +1,227 @@
/**
* Tests for ensureOnboardingComplete function in cli-integration-handler.ts
*
* Tests the exported ensureOnboardingComplete() which reads/writes .claude.json
* to set hasCompletedOnboarding: true, suppressing Claude's onboarding wizard
* for already-authenticated profiles.
*/
import { describe, test, expect, vi, beforeEach } from 'vitest';
import * as path from 'path';
import * as os from 'os';
// ---- fs mock (sync only — the function uses fs, not fs/promises) ----
const mockFiles: Map<string, string | Error> = new Map();
vi.mock('fs', () => {
const readFileSync = vi.fn((filePath: string, _encoding?: string): string => {
const entry = mockFiles.get(filePath);
if (entry === undefined) {
const err = new Error(`ENOENT: no such file or directory, open '${filePath}'`) as NodeJS.ErrnoException;
err.code = 'ENOENT';
throw err;
}
if (entry instanceof Error) {
throw entry;
}
return entry;
});
const writeFileSync = vi.fn();
const renameSync = vi.fn();
return { default: { readFileSync, writeFileSync, renameSync }, readFileSync, writeFileSync, renameSync };
});
// ---- stubs for heavy transitive dependencies ----
vi.mock('electron', () => ({
ipcMain: { handle: vi.fn() },
app: { getPath: vi.fn(() => os.tmpdir()), getAppPath: vi.fn(() => os.tmpdir()) },
dialog: { showOpenDialog: vi.fn() },
shell: { openExternal: vi.fn() },
}));
vi.mock('@electron-toolkit/utils', () => ({ is: { dev: true } }));
vi.mock('../../shared/constants', async () => {
const actual = await vi.importActual<typeof import('../../shared/constants')>('../../shared/constants');
return { ...actual };
});
vi.mock('../claude-profile-manager', () => ({
getClaudeProfileManager: vi.fn(),
initializeClaudeProfileManager: vi.fn(),
}));
vi.mock('../claude-profile/credential-utils', () => ({
getFullCredentialsFromKeychain: vi.fn(),
clearKeychainCache: vi.fn(),
updateProfileSubscriptionMetadata: vi.fn(),
}));
vi.mock('../claude-profile/usage-monitor', () => ({
getUsageMonitor: vi.fn(),
}));
vi.mock('../claude-profile/profile-utils', () => ({
getEmailFromConfigDir: vi.fn(),
}));
vi.mock('../terminal/output-parser', () => ({}));
vi.mock('../terminal/session-handler', () => ({}));
vi.mock('./pty-manager', () => ({
writeToPty: vi.fn(),
resizePty: vi.fn(),
}));
vi.mock('../ipc-handlers/utils', () => ({
safeSendToRenderer: vi.fn(),
}));
vi.mock('../../shared/utils/debug-logger', () => ({
debugLog: vi.fn(),
debugError: vi.fn(),
}));
vi.mock('../../shared/utils/shell-escape', () => ({
escapeShellArg: vi.fn((s: string) => s),
escapeForWindowsDoubleQuote: vi.fn((s: string) => s),
buildCdCommand: vi.fn((cwd: string) => `cd ${cwd}`),
}));
vi.mock('../cli-utils', () => ({
getClaudeCliInvocation: vi.fn(() => 'claude'),
getClaudeCliInvocationAsync: vi.fn(async () => 'claude'),
}));
vi.mock('../platform', () => ({
isWindows: vi.fn(() => false),
}));
vi.mock('../settings-utils', () => ({
readSettingsFileAsync: vi.fn(async () => ({})),
readSettingsFile: vi.fn(() => ({})),
}));
// ---- import the function under test ----
import { ensureOnboardingComplete } from '../terminal/cli-integration-handler';
import * as fs from 'fs';
// ---- helpers ----
function claudeJsonPath(configDir: string): string {
const expanded = configDir.startsWith('~')
? configDir.replace(/^~/, os.homedir())
: configDir;
return path.join(path.resolve(expanded), '.claude.json');
}
const TEST_DIR = '/tmp/test-profile';
describe('ensureOnboardingComplete', () => {
beforeEach(() => {
vi.clearAllMocks();
mockFiles.clear();
});
// ---- ENOENT: file does not exist ----
test('returns early (no write) when .claude.json does not exist', () => {
// mockFiles is empty → readFileSync will throw ENOENT
ensureOnboardingComplete(TEST_DIR);
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
// ---- already set ----
test('returns early (no write) when hasCompletedOnboarding is already true', () => {
const filePath = claudeJsonPath(TEST_DIR);
mockFiles.set(filePath, JSON.stringify({ hasCompletedOnboarding: true }));
ensureOnboardingComplete(TEST_DIR);
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
// ---- missing flag → should write ----
test('writes hasCompletedOnboarding: true when flag is absent', () => {
const filePath = claudeJsonPath(TEST_DIR);
mockFiles.set(filePath, JSON.stringify({ someOtherField: 'value' }));
ensureOnboardingComplete(TEST_DIR);
expect(fs.writeFileSync).toHaveBeenCalledOnce();
const written = JSON.parse((fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls[0][1] as string);
expect(written.hasCompletedOnboarding).toBe(true);
expect(written.someOtherField).toBe('value');
});
// ---- flag is false → should write ----
test('writes hasCompletedOnboarding: true when flag is false', () => {
const filePath = claudeJsonPath(TEST_DIR);
mockFiles.set(filePath, JSON.stringify({ hasCompletedOnboarding: false }));
ensureOnboardingComplete(TEST_DIR);
expect(fs.writeFileSync).toHaveBeenCalledOnce();
const written = JSON.parse((fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls[0][1] as string);
expect(written.hasCompletedOnboarding).toBe(true);
});
// ---- non-object JSON (string) → should return silently ----
test('returns early (no write) when .claude.json contains a JSON string', () => {
const filePath = claudeJsonPath(TEST_DIR);
mockFiles.set(filePath, JSON.stringify('just a string'));
ensureOnboardingComplete(TEST_DIR);
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
// ---- array JSON → should return silently ----
test('returns early (no write) when .claude.json contains a JSON array', () => {
const filePath = claudeJsonPath(TEST_DIR);
mockFiles.set(filePath, JSON.stringify([1, 2, 3]));
ensureOnboardingComplete(TEST_DIR);
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
// ---- corrupted / invalid JSON → outer catch swallows error ----
test('handles corrupted JSON gracefully without throwing', () => {
const filePath = claudeJsonPath(TEST_DIR);
mockFiles.set(filePath, '{ invalid json }');
expect(() => ensureOnboardingComplete(TEST_DIR)).not.toThrow();
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
// ---- tilde expansion ----
test('expands leading tilde to home directory', () => {
const tildeDir = '~/myprofile';
const resolvedDir = path.resolve(tildeDir.replace(/^~/, os.homedir()));
const filePath = path.join(resolvedDir, '.claude.json');
mockFiles.set(filePath, JSON.stringify({}));
ensureOnboardingComplete(tildeDir);
expect(fs.writeFileSync).toHaveBeenCalledOnce();
// Writes to a temp file (claudeJsonPath + UUID + .tmp), then renames to target
const writtenPath = (fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
expect(writtenPath).toMatch(new RegExp(`^${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\..*\\.tmp$`));
expect(fs.renameSync).toHaveBeenCalledWith(writtenPath, filePath);
});
// ---- write error → outer catch swallows error ----
test('handles write error gracefully without throwing', () => {
const filePath = claudeJsonPath(TEST_DIR);
mockFiles.set(filePath, JSON.stringify({}));
(fs.writeFileSync as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
throw new Error('EACCES: permission denied');
});
expect(() => ensureOnboardingComplete(TEST_DIR)).not.toThrow();
});
});
@@ -40,8 +40,8 @@ const __dirname = path.dirname(__filename);
*/
function resolveWorkerPath(): string {
if (app.isPackaged) {
// Production: worker is bundled alongside other main-process code
return path.join(process.resourcesPath, 'app', 'main', 'ai', 'agent', 'worker.js');
// Production: worker is inside app.asar at out/main/ai/agent/worker.js
return path.join(process.resourcesPath, 'app.asar', 'out', 'main', 'ai', 'agent', 'worker.js');
}
// Dev: electron-vite outputs worker at out/main/ai/agent/worker.js
// because the Rollup input key is 'ai/agent/worker'.
+1 -1
View File
@@ -253,7 +253,7 @@ export async function startCodexOAuthFlow(): Promise<CodexAuthResult> {
<body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #1a1a1a; color: #e0e0e0;">
<div style="text-align: center;">
<h2 style="color: #4ade80;">Authentication successful!</h2>
<p>You can close this tab and return to Auto Claude.</p>
<p>You can close this tab and return to Aperant.</p>
</div>
</body>
</html>`;
+1
View File
@@ -147,6 +147,7 @@ export const MODEL_PROVIDER_MAP: Record<string, SupportedProvider> = {
'llama-': 'groq',
'grok-': 'xai',
'glm-': 'zai',
'mascarade-': 'mascarade',
} as const;
// ============================================
+1 -1
View File
@@ -106,7 +106,7 @@ const PUPPETEER_SERVER: McpServerConfig = {
function createAutoClaudeServer(specDir: string): McpServerConfig {
return {
id: 'auto-claude',
name: 'Auto-Claude',
name: 'Aperant',
description: 'Build management tools (progress tracking, session context)',
enabledByDefault: true,
transport: {
+45 -5
View File
@@ -7,11 +7,51 @@
* 3. Web app (Next.js SaaS) — pure cloud libSQL
*/
import { createClient } from '@libsql/client';
import type { Client } from '@libsql/client';
import type { Client, Config } from '@libsql/client/sqlite3';
import { createRequire } from 'module';
import { join } from 'path';
import { MEMORY_SCHEMA_SQL, MEMORY_PRAGMA_SQL } from './schema';
/**
* Lazy-load @libsql/client via CJS require().
*
* @libsql/client depends on native platform-specific modules (@libsql/darwin-arm64,
* @libsql/linux-x64-gnu, etc.). In packaged Electron apps these live in
* Resources/node_modules/ (via extraResources). ESM import() can't resolve them
* from within app.asar, but CJS require() works because Module.globalPaths is
* patched at startup in index.ts to include Resources/node_modules/.
*
* Using a lazy getter avoids a static import that would crash at startup before
* the globalPaths patch runs.
*/
let _createClient: ((config: Config) => Client) | null = null;
function loadCreateClient(): (config: Config) => Client {
if (!_createClient) {
// In Electron: globalThis.require is set up in index.ts with Module.globalPaths
// patched to include Resources/node_modules/ for extraResources packages.
// In tests/dev: fall back to createRequire (deps are in normal node_modules).
const req = globalThis.require ?? createRequire(import.meta.url);
let mod: Record<string, unknown>;
try {
mod = req('@libsql/client/sqlite3');
} catch (err) {
throw new Error(
`Failed to load @libsql/client/sqlite3: ${(err as Error).message}. ` +
`Ensure native modules are available in Resources/node_modules/`
);
}
if (typeof mod.createClient !== 'function') {
throw new Error(
`@libsql/client/sqlite3 did not export createClient (got ${typeof mod.createClient}). ` +
`Check that native modules are available in Resources/node_modules/`
);
}
_createClient = mod.createClient as (config: Config) => Client;
}
return _createClient!;
}
let _client: Client | null = null;
/**
@@ -31,7 +71,7 @@ export async function getMemoryClient(
const { app } = await import('electron');
const localPath = join(app.getPath('userData'), 'memory.db');
_client = createClient({
_client = loadCreateClient()({
url: `file:${localPath}`,
...(tursoSyncUrl && authToken
? { syncUrl: tursoSyncUrl, authToken, syncInterval: 60 }
@@ -78,7 +118,7 @@ export async function getWebMemoryClient(
tursoUrl: string,
authToken: string,
): Promise<Client> {
const client = createClient({ url: tursoUrl, authToken });
const client = loadCreateClient()({ url: tursoUrl, authToken });
// Apply PRAGMAs
for (const pragma of MEMORY_PRAGMA_SQL.split('\n').filter(l => l.trim())) {
@@ -97,7 +137,7 @@ export async function getWebMemoryClient(
* Create an in-memory client (for tests — no Electron dependency).
*/
export async function getInMemoryClient(): Promise<Client> {
const client = createClient({ url: ':memory:' });
const client = loadCreateClient()({ url: ':memory:' });
await client.executeMultiple(MEMORY_SCHEMA_SQL);
return client;
}
+1 -1
View File
@@ -251,7 +251,7 @@ export interface MemoryMethodologyPlugin {
export const nativePlugin: MemoryMethodologyPlugin = {
id: 'native',
displayName: 'Auto Claude (Subtasks)',
displayName: 'Aperant (Subtasks)',
mapPhase: (p: string): UniversalPhase => {
const map: Record<string, UniversalPhase> = {
planning: 'define',
@@ -156,6 +156,24 @@ function createProviderInstance(config: ProviderConfig) {
});
}
case SupportedProvider.Mascarade: {
// Mascarade LLM orchestration engine — OpenAI-compatible API
// Default: http://localhost:8100/v1 (Tower) or configurable
let mascaradeBaseURL = baseURL ?? 'http://localhost:8100/v1';
if (!mascaradeBaseURL.endsWith('/v1')) {
mascaradeBaseURL = mascaradeBaseURL.replace(/\/+$/, '') + '/v1';
}
return createOpenAICompatible({
name: 'mascarade',
apiKey: apiKey ?? 'mascarade-local',
baseURL: mascaradeBaseURL,
headers: {
...headers,
'Authorization': `Bearer ${apiKey ?? 'mascarade-local'}`,
},
});
}
default: {
const _exhaustive: never = provider;
throw new Error(`Unsupported provider: ${_exhaustive}`);
@@ -21,6 +21,7 @@ export const SupportedProvider = {
OpenRouter: 'openrouter',
ZAI: 'zai',
Ollama: 'ollama',
Mascarade: 'mascarade',
} as const;
export type SupportedProvider = (typeof SupportedProvider)[keyof typeof SupportedProvider];
@@ -382,7 +382,7 @@ ${diffContent}
}
lines.push('---');
lines.push('_Generated by Auto Claude MR Review_');
lines.push('_Generated by Aperant MR Review_');
return lines.join('\n');
}
@@ -87,7 +87,7 @@ const BLOCKED_PROCESS_NAMES = new Set([
'electron',
'Electron',
'auto-claude',
'Auto Claude',
'Aperant',
]);
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -149,7 +149,7 @@ export function generateDebugReport(): string {
const recentErrors = getRecentErrors(10);
const lines = [
'=== Auto Claude Debug Report ===',
'=== Aperant Debug Report ===',
`Generated: ${new Date().toISOString()}`,
'',
'--- System Information ---',
+2 -2
View File
@@ -30,7 +30,7 @@ import { isMacOS } from './platform';
// GitHub repo info for API calls
const GITHUB_OWNER = 'AndyMik90';
const GITHUB_REPO = 'Auto-Claude';
const GITHUB_REPO = 'Aperant';
// Debug mode - DEBUG_UPDATER=true or development mode
const DEBUG_UPDATER = process.env.DEBUG_UPDATER === 'true' || process.env.NODE_ENV === 'development';
@@ -488,7 +488,7 @@ async function fetchLatestStableRelease(): Promise<AppUpdateInfo | null> {
});
request.setHeader('Accept', 'application/vnd.github.v3+json');
request.setHeader('User-Agent', `Auto-Claude/${getCurrentVersion()}`);
request.setHeader('User-Agent', `Aperant/${getCurrentVersion()}`);
let data = '';
+36 -3
View File
@@ -3,11 +3,24 @@
// require-in-the-middle hooks. Sentry's hooks expect require.cache to exist,
// which is only available in CommonJS. Without this, node-pty native module
// loading fails with "ReferenceError: require is not defined".
import { createRequire } from 'module';
import Module, { createRequire } from 'module';
const require = createRequire(import.meta.url);
// Make require globally available for Sentry's require-in-the-middle hooks
globalThis.require = require;
// In packaged Electron apps, native modules (e.g. @libsql/client) are placed in
// Resources/node_modules/ via extraResources. Add that path to CJS resolution so
// globalThis.require() can find them at runtime.
if (process.resourcesPath) {
const nativeModulesPath = require('path').join(process.resourcesPath, 'node_modules');
// Module.globalPaths is an undocumented but stable Node.js internal used for
// CJS module resolution. It's not in @types/node, hence the cast.
const globalPaths = (Module as unknown as { globalPaths: string[] }).globalPaths;
if (!globalPaths.includes(nativeModulesPath)) {
globalPaths.push(nativeModulesPath);
}
}
// Load .env file FIRST before any other imports that might use process.env
import { config } from 'dotenv';
import { resolve, dirname } from 'path';
@@ -37,7 +50,7 @@ for (const envPath of possibleEnvPaths) {
import { app, BrowserWindow, shell, nativeImage, session, screen, Menu, MenuItem } from 'electron';
import { join } from 'path';
import { accessSync, readFileSync, writeFileSync, rmSync } from 'fs';
import { accessSync, readFileSync, writeFileSync, rmSync, cpSync } from 'fs';
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
import { setupIpcHandlers } from './ipc-setup';
import { AgentManager } from './agent';
@@ -58,6 +71,26 @@ import { isMacOS, isWindows } from './platform';
import { ptyDaemonClient } from './terminal/pty-daemon-client';
import type { AppSettings, AuthFailureInfo } from '../shared/types';
// ─────────────────────────────────────────────────────────────────────────────
// Migrate userData from old app name (auto-claude-ui → aperant)
// Must run before any code accesses app.getPath('userData')
// ─────────────────────────────────────────────────────────────────────────────
{
const newUserData = app.getPath('userData');
const oldUserData = join(dirname(newUserData), 'auto-claude-ui');
if (existsSync(oldUserData) && !existsSync(join(newUserData, '.migrated'))) {
try {
// Copy all files from old location to new (don't move — keeps old as backup)
cpSync(oldUserData, newUserData, { recursive: true, force: false, errorOnExist: false });
// Mark as migrated so we don't repeat
writeFileSync(join(newUserData, '.migrated'), new Date().toISOString());
console.warn('[main] Migrated userData from auto-claude-ui to aperant');
} catch (err) {
console.warn('[main] userData migration failed (non-fatal):', err);
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Window sizing constants
// ─────────────────────────────────────────────────────────────────────────────
@@ -373,7 +406,7 @@ if (isWindows()) {
// Initialize the application
app.whenReady().then(() => {
// Set app user model id for Windows
electronApp.setAppUserModelId('com.autoclaude.ui');
electronApp.setAppUserModelId('com.aperant.app');
// Clear cache on Windows to prevent permission errors from stale cache
if (isWindows()) {
@@ -129,7 +129,7 @@ async function githubGraphQL<T>(
headers: {
"Authorization": `Bearer ${safeToken}`,
"Content-Type": "application/json",
"User-Agent": "Auto-Claude-UI",
"User-Agent": "Aperant",
},
body: JSON.stringify({ query, variables }),
});
@@ -2275,7 +2275,7 @@ export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): v
if (options?.forceApprove) {
// Auto-approve format: clean approval message with optional suggestions
body = `## ✅ Auto Claude Review - APPROVED\n\n`;
body = `## ✅ Aperant Review - APPROVED\n\n`;
body += `**Status:** Ready to Merge\n\n`;
body += `**Summary:** ${result.summary}\n\n`;
@@ -2298,10 +2298,10 @@ export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): v
}
body += `---\n*This automated review found no blocking issues. The PR can be safely merged.*\n\n`;
body += `*Generated by Auto Claude*`;
body += `*Generated by Aperant*`;
} else {
// Standard review format
body = `## 🤖 Auto Claude PR Review\n\n${result.summary}\n\n`;
body = `## 🤖 Aperant PR Review\n\n${result.summary}\n\n`;
if (findings.length > 0) {
// Show selected count vs total if filtered
@@ -2326,7 +2326,7 @@ export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): v
body += `*No findings selected for this review.*\n\n`;
}
body += `---\n*This review was generated by Auto Claude.*`;
body += `---\n*This review was generated by Aperant.*`;
}
// Determine review status based on selected findings (or force approve)
@@ -267,7 +267,7 @@ export async function githubFetch(
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${safeToken}`,
'User-Agent': 'Auto-Claude-UI',
'User-Agent': 'Aperant',
...options.headers
}
});
@@ -298,7 +298,7 @@ export async function githubFetchWithETag(
const headers: Record<string, string> = {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${token}`,
'User-Agent': 'Auto-Claude-UI'
'User-Agent': 'Aperant'
};
// Add If-None-Match header if we have a cached ETag
@@ -108,7 +108,7 @@ function formatReviewBody(result: MRReviewResult, selectedFindingIds?: string[])
? result.findings.filter(f => selectedSet.has(f.id))
: result.findings;
let body = `## Auto Claude MR Review\n\n${result.summary}\n\n`;
let body = `## Aperant MR Review\n\n${result.summary}\n\n`;
if (findings.length > 0) {
const countText = selectedSet
@@ -130,7 +130,7 @@ function formatReviewBody(result: MRReviewResult, selectedFindingIds?: string[])
body += `*No findings selected for this review.*\n\n`;
}
body += `---\n*This review was generated by Auto Claude.*`;
body += `---\n*This review was generated by Aperant.*`;
return body;
}
@@ -335,7 +335,7 @@ describe('GitLab MR Review Handlers', () => {
it('should format review header', () => {
const body = formatReviewBody(baseResult);
expect(body).toContain('## Auto Claude MR Review');
expect(body).toContain('## Aperant MR Review');
expect(body).toContain('Found 2 issues that need attention');
});
@@ -410,7 +410,7 @@ describe('GitLab MR Review Handlers', () => {
const body = formatReviewBody(baseResult);
expect(body).toContain('---');
expect(body).toContain('*This review was generated by Auto Claude.*');
expect(body).toContain('*This review was generated by Aperant.*');
});
it('should format finding descriptions', () => {
@@ -520,7 +520,7 @@ export function registerMRReviewHandlers(
debugLog('Posting findings', { total: result.findings.length, selected: findings.length });
// Build note body
let body = `## Auto Claude MR Review\n\n${result.summary}\n\n`;
let body = `## Aperant MR Review\n\n${result.summary}\n\n`;
if (findings.length > 0) {
const countText = selectedSet
@@ -542,7 +542,7 @@ export function registerMRReviewHandlers(
body += `*No findings selected for this review.*\n\n`;
}
body += `---\n*This review was generated by Auto Claude.*`;
body += `---\n*This review was generated by Aperant.*`;
const encodedProject = encodeProjectPath(config.project);
@@ -133,7 +133,7 @@ export function registerInsightsHandlers(getMainWindow: () => BrowserWindow | nu
}
if (!project.autoBuildPath) {
return { success: false, error: "Auto Claude not initialized for this project" };
return { success: false, error: "Aperant not initialized for this project" };
}
try {
@@ -264,7 +264,7 @@ const detectAutoBuildSourcePath = (): string | null => {
}
}
console.warn('[detectAutoBuildSourcePath] Could not auto-detect Auto Claude prompts path. Please configure manually in settings.');
console.warn('[detectAutoBuildSourcePath] Could not auto-detect Aperant prompts path. Please configure manually in settings.');
console.warn('[detectAutoBuildSourcePath] Set DEBUG=1 environment variable for detailed path checking.');
return null;
};
@@ -933,6 +933,20 @@ export function registerSettingsHandlers(
try {
const settings = readSettingsFile() ?? {};
const accounts: ProviderAccount[] = (settings.providerAccounts as ProviderAccount[] | undefined) ?? [];
// Prevent duplicate: same email + provider already registered
if (account.email) {
const duplicate = accounts.find(
(a) => a.provider === account.provider && a.email?.toLowerCase() === account.email!.toLowerCase()
);
if (duplicate) {
return {
success: false,
error: `DUPLICATE_EMAIL:${duplicate.name}`,
};
}
}
const now = Date.now();
const newAccount: ProviderAccount = {
...account,
@@ -177,7 +177,7 @@ export function registerTaskExecutionHandlers(
mainWindow.webContents.send(
IPC_CHANNELS.TASK_ERROR,
taskId,
'Git repository required. Please run "git init" in your project directory. Auto Claude uses git worktrees for isolated builds.'
'Git repository required. Please run "git init" in your project directory. Aperant uses git worktrees for isolated builds.'
);
return;
}
+5 -5
View File
@@ -205,14 +205,14 @@ function ensureGitignoreEntries(projectPath: string, entries: string[]): void {
appendContent += '\n';
}
appendContent += '\n# Auto Claude data directory\n';
appendContent += '\n# Aperant data directory\n';
for (const entry of entriesToAdd) {
appendContent += entry + '\n';
}
appendFileSync(gitignorePath, appendContent);
} else {
writeFileSync(gitignorePath, '# Auto Claude data directory\n' + entriesToAdd.join('\n') + '\n', 'utf-8');
writeFileSync(gitignorePath, '# Aperant data directory\n' + entriesToAdd.join('\n') + '\n', 'utf-8');
}
debug('Added entries to .gitignore', { entries: entriesToAdd });
@@ -288,13 +288,13 @@ export function initializeProject(projectPath: string): InitializationResult {
};
}
// Check git status - Auto Claude requires git for worktree-based builds
// Check git status - Aperant requires git for worktree-based builds
const gitStatus = checkGitStatus(projectPath);
if (!gitStatus.isGitRepo || !gitStatus.hasCommits) {
debug('Git check failed', { gitStatus });
return {
success: false,
error: gitStatus.error || 'Git repository required. Auto Claude uses git worktrees for isolated builds.'
error: gitStatus.error || 'Git repository required. Aperant uses git worktrees for isolated builds.'
};
}
@@ -375,7 +375,7 @@ export function ensureDataDirectories(projectPath: string): InitializationResult
*
* IMPORTANT: Only .auto-claude/ is considered a valid "installed" auto-claude.
* The auto-claude/ folder (if it exists) is the SOURCE CODE being developed,
* not an installation. This allows Auto Claude to be used to develop itself.
* not an installation. This allows Aperant to be used to develop itself.
*/
export function getAutoBuildPath(projectPath: string): string | null {
const dotAutoBuildPath = path.join(projectPath, '.auto-claude');
@@ -27,8 +27,7 @@ import type {
TerminalProcess,
WindowGetter,
RateLimitEvent,
OAuthTokenEvent,
OnboardingCompleteEvent
OAuthTokenEvent
} from './types';
// ============================================================================
@@ -564,17 +563,17 @@ export function handleOAuthToken(
console.warn('[ClaudeIntegration] Profile credentials verified via Keychain (not caching token):', profileId);
// Set flag to watch for Claude's ready state (onboarding complete)
terminal.awaitingOnboardingComplete = true;
// Mark onboarding complete so future `claude` invocations skip the wizard.
// `claude auth login` creates .claude.json but doesn't set this flag.
if (profile.configDir) {
ensureOnboardingComplete(profile.configDir);
}
// needsOnboarding: true tells the UI to show "complete setup" message
// instead of "success" - user should finish Claude's onboarding before closing
safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
terminalId: terminal.id,
profileId,
email: emailFromOutput || keychainCreds.email || profile?.email,
success: true,
needsOnboarding: true,
detectedAt: new Date().toISOString()
} as OAuthTokenEvent);
} else {
@@ -585,17 +584,16 @@ export function handleOAuthToken(
if (hasCredentials) {
console.warn('[ClaudeIntegration] Profile credentials verified (no Keychain token):', profileId);
// Set flag to watch for Claude's ready state (onboarding complete)
terminal.awaitingOnboardingComplete = true;
// Mark onboarding complete so future `claude` invocations skip the wizard
if (profile.configDir) {
ensureOnboardingComplete(profile.configDir);
}
// needsOnboarding: true tells the UI to show "complete setup" message
// instead of "success" - user should finish Claude's onboarding before closing
safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
terminalId: terminal.id,
profileId,
email: emailFromOutput || profile?.email,
success: true,
needsOnboarding: true,
detectedAt: new Date().toISOString()
} as OAuthTokenEvent);
} else {
@@ -718,130 +716,19 @@ export function handleOAuthToken(
/**
* Handle onboarding complete detection
* Called when terminal output indicates Claude Code is ready after login/onboarding
* Called when terminal output indicates Claude Code is ready after login/onboarding.
*
* This detects the Claude Code welcome screen that appears after successful login,
* which includes patterns like "Welcome back", "Claude Code v2.x", or subscription
* tier info like "Claude Max". When detected, it notifies the frontend to auto-close
* the auth terminal.
* Note: This is now a no-op. The onboarding flag is set proactively via
* ensureOnboardingComplete() in handleOAuthToken() and executeProfileCommand(),
* so awaitingOnboardingComplete is never set and this path is never reached.
* Kept as a stub to satisfy the terminal-event-handler callback interface.
*/
export function handleOnboardingComplete(
terminal: TerminalProcess,
data: string,
getWindow: WindowGetter
_terminal: TerminalProcess,
_data: string,
_getWindow: WindowGetter
): void {
// Only check if we're waiting for onboarding to complete
if (!terminal.awaitingOnboardingComplete) {
return;
}
// Check if output shows Claude Code welcome screen (onboarding complete indicators)
if (!OutputParser.isOnboardingCompleteOutput(data)) {
return;
}
console.warn('[ClaudeIntegration] Onboarding complete detected for terminal:', terminal.id);
// Clear the flag
terminal.awaitingOnboardingComplete = false;
// Extract profile ID from terminal ID pattern (claude-login-{profileId}-*)
const profileId = extractProfileIdFromAuthTerminalId(terminal.id) || undefined;
// Try to extract email from the welcome screen (e.g., "user@example.com's Organization")
// Note: extractEmail automatically strips ANSI escape codes internally
let email = OutputParser.extractEmail(data);
if (!email) {
email = OutputParser.extractEmail(terminal.outputBuffer);
}
// Fallback: If terminal extraction failed or might be corrupt, read directly from Claude's config file
// This is the authoritative source and doesn't suffer from ANSI escape code issues
const profileManager = getClaudeProfileManager();
const profile = profileId ? profileManager.getProfile(profileId) : null;
if (!email && profile?.configDir) {
const configEmail = getEmailFromConfigDir(profile.configDir);
if (configEmail) {
console.warn('[ClaudeIntegration] Email not found in terminal output, using config file:', maskEmail(configEmail));
email = configEmail;
}
}
// Validate email looks correct (basic sanity check)
// If terminal extraction gave us a truncated email but config file has the correct one, prefer config
if (email && profile?.configDir) {
const configEmail = getEmailFromConfigDir(profile.configDir);
if (configEmail && configEmail !== email) {
// Config file email is different - it's more authoritative
console.warn('[ClaudeIntegration] Terminal email differs from config file, using config file:', {
terminalEmail: maskEmail(email),
configEmail: maskEmail(configEmail)
});
email = configEmail;
}
}
console.warn('[ClaudeIntegration] Email extraction attempt:', {
profileId,
foundEmail: maskEmail(email),
dataLength: data.length,
bufferLength: terminal.outputBuffer.length
});
// Update profile with email and subscription metadata if found and profile exists
// Always update - the newly extracted email from re-authentication should overwrite any stale/truncated email
if (profileId && email && profile) {
const previousEmail = profile.email;
profile.email = email;
// Also update subscription metadata from Keychain credentials
updateProfileSubscriptionMetadata(profile, profile.configDir);
profileManager.saveProfile(profile);
if (previousEmail !== email) {
console.warn('[ClaudeIntegration] Updated profile email from welcome screen:', profileId, maskEmail(email), '(was:', maskEmail(previousEmail), ')');
}
}
safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, {
terminalId: terminal.id,
profileId,
detectedAt: new Date().toISOString()
} as OnboardingCompleteEvent);
// Trigger immediate usage fetch after successful re-authentication
// This gives the user immediate feedback that their account is working
if (profileId) {
try {
const usageMonitor = getUsageMonitor();
if (usageMonitor) {
// Clear any auth failure status for this profile since they just re-authenticated
usageMonitor.clearAuthFailedProfile(profileId);
console.warn('[ClaudeIntegration] Triggering immediate usage fetch after re-authentication:', profileId);
// Switch to this profile if it's not already active, then fetch usage
const profileManager = getClaudeProfileManager();
// Also clear the migration flag if this profile was migrated to an isolated directory
// This prevents the auth failure modal from showing again on next startup
if (profileManager.isProfileMigrated(profileId)) {
profileManager.clearMigratedProfile(profileId);
console.warn('[ClaudeIntegration] Cleared migration flag for re-authenticated profile:', profileId);
}
const activeProfile = profileManager.getActiveProfile();
if (activeProfile?.id !== profileId) {
profileManager.setActiveProfile(profileId);
}
// Small delay to allow profile switch to settle, then trigger usage fetch
setTimeout(() => {
usageMonitor.checkNow();
}, 500);
}
} catch (error) {
console.error('[ClaudeIntegration] Failed to trigger post-auth usage fetch:', error);
}
}
// No-op — onboarding is handled proactively in handleOAuthToken()
}
/**
@@ -893,6 +780,54 @@ export function handleClaudeExit(
safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_CLAUDE_EXIT, terminal.id);
}
/**
* Ensure hasCompletedOnboarding is set in profile's .claude.json.
* When CLAUDE_CONFIG_DIR is set, Claude Code reads .claude.json from that directory.
* Without this flag, it triggers the onboarding wizard even for authenticated profiles.
*/
export function ensureOnboardingComplete(configDir: string): void {
try {
const expandedDir = path.resolve(
configDir.startsWith('~') ? configDir.replace(/^~/, os.homedir()) : configDir
);
const claudeJsonPath = path.join(expandedDir, '.claude.json');
// Read directly instead of existsSync + readFileSync to avoid TOCTOU race (CodeQL js/file-system-race)
let content: string;
try {
content = fs.readFileSync(claudeJsonPath, 'utf-8');
} catch (readErr) {
if ((readErr as NodeJS.ErrnoException).code === 'ENOENT') {
return; // No .claude.json yet — Claude Code will create it during auth
}
throw readErr;
}
const config = JSON.parse(content);
if (typeof config !== 'object' || config === null || Array.isArray(config)) {
return; // Not a valid config object
}
if (config.hasCompletedOnboarding === true) {
return; // Already set
}
config.hasCompletedOnboarding = true;
const updatedContent = JSON.stringify(config, null, 2);
// Write atomically via temp file + rename to avoid partial writes and satisfy CodeQL js/insecure-temporary-file.
// crypto.randomUUID() ensures no collisions; mode 0o600 restricts to owner-only.
const tmpPath = `${claudeJsonPath}.${crypto.randomUUID()}.tmp`;
fs.writeFileSync(tmpPath, updatedContent, { encoding: 'utf-8', mode: 0o600 });
fs.renameSync(tmpPath, claudeJsonPath);
debugLog(`[ClaudeIntegration] Set hasCompletedOnboarding in ${claudeJsonPath}`);
} catch (error) {
// Non-fatal — worst case the user sees onboarding once
debugError('[ClaudeIntegration] Failed to set hasCompletedOnboarding:', error);
}
}
/**
* Shared command execution logic for profile-based invocation
* Returns true if command was executed via configDir or temp-file method
@@ -938,6 +873,9 @@ function executeProfileCommand(options: ExecuteProfileCommandOptions): boolean {
// read full Keychain credentials including subscriptionType ("max") and rateLimitTier.
// Using CLAUDE_CODE_OAUTH_TOKEN alone lacks tier info, causing "Claude API" display.
if (activeProfile.configDir) {
// Ensure Claude Code skips onboarding for authenticated profiles
ensureOnboardingComplete(activeProfile.configDir);
const command = buildClaudeShellCommand(
cwdCommand,
pathPrefix,
@@ -1016,6 +954,9 @@ async function executeProfileCommandAsync(options: ExecuteProfileCommandOptions)
// read full Keychain credentials including subscriptionType ("max") and rateLimitTier.
// Using CLAUDE_CODE_OAUTH_TOKEN alone lacks tier info, causing "Claude API" display.
if (activeProfile.configDir) {
// Ensure Claude Code skips onboarding for authenticated profiles
ensureOnboardingComplete(activeProfile.configDir);
const command = buildClaudeShellCommand(
cwdCommand,
pathPrefix,
-13
View File
@@ -26,8 +26,6 @@ export interface TerminalProcess {
dangerouslySkipPermissions?: boolean;
/** Shell type for Windows (affects command chaining syntax) */
shellType?: WindowsShellType;
/** Whether this terminal is waiting for Claude onboarding to complete (login flow) */
awaitingOnboardingComplete?: boolean;
/** Whether PTY has emitted exit; used to avoid writes/resizes on dead PTYs */
hasExited?: boolean;
}
@@ -55,19 +53,8 @@ export interface OAuthTokenEvent {
success: boolean;
message?: string;
detectedAt: string;
/** If true, user should complete onboarding in terminal before closing */
needsOnboarding?: boolean;
}
/**
* Onboarding complete event data
* Sent when Claude Code shows its ready state after login/onboarding
*/
export interface OnboardingCompleteEvent {
terminalId: string;
profileId?: string;
detectedAt: string;
}
/**
* Session capture result
+1 -19
View File
@@ -80,7 +80,7 @@ export interface TerminalAPI {
onTerminalClaudeSession: (callback: (id: string, sessionId: string) => void) => () => void;
onTerminalRateLimit: (callback: (info: RateLimitInfo) => void) => () => void;
onTerminalOAuthToken: (
callback: (info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string; needsOnboarding?: boolean }) => void
callback: (info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string }) => void
) => () => void;
onTerminalAuthCreated: (
callback: (info: { terminalId: string; profileId: string; profileName: string }) => void
@@ -91,9 +91,6 @@ export interface TerminalAPI {
submitOAuthCode: (terminalId: string, code: string) => Promise<IPCResult>;
onTerminalClaudeBusy: (callback: (id: string, isBusy: boolean) => void) => () => void;
onTerminalClaudeExit: (callback: (id: string) => void) => () => void;
onTerminalOnboardingComplete: (
callback: (info: { terminalId: string; profileId?: string; detectedAt: string }) => void
) => () => void;
onTerminalPendingResume: (callback: (id: string, sessionId?: string) => void) => () => void;
onTerminalProfileChanged: (callback: (event: TerminalProfileChangedEvent) => void) => () => void;
@@ -388,21 +385,6 @@ export const createTerminalAPI = (): TerminalAPI => ({
};
},
onTerminalOnboardingComplete: (
callback: (info: { terminalId: string; profileId?: string; detectedAt: string }) => void
): (() => void) => {
const handler = (
_event: Electron.IpcRendererEvent,
info: { terminalId: string; profileId?: string; detectedAt: string }
): void => {
callback(info);
};
ipcRenderer.on(IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, handler);
return () => {
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, handler);
};
},
onTerminalPendingResume: (
callback: (id: string, sessionId?: string) => void
): (() => void) => {
+1 -1
View File
@@ -747,7 +747,7 @@ export function App() {
} else {
// Initialization failed - show error but keep dialog open
console.warn('[InitDialog] Initialization failed, showing error');
const errorMessage = result?.error || 'Failed to initialize Auto Claude. Please try again.';
const errorMessage = result?.error || 'Failed to initialize Aperant. Please try again.';
setInitError(errorMessage);
setIsInitializing(false);
}
@@ -287,7 +287,7 @@ const MCP_SERVERS: Record<string, { name: string; description: string; icon: Rea
],
},
'auto-claude': {
name: 'Auto-Claude Tools',
name: 'Aperant Tools',
description: 'Build progress tracking, session context, discoveries & gotchas recording',
icon: ListChecks,
tools: [
@@ -180,7 +180,7 @@ export function AppUpdateNotification() {
<DialogDescription>
{t(
"dialogs:appUpdate.description",
"A new version of Auto Claude is ready to download"
"A new version of Aperant is ready to download"
)}
</DialogDescription>
</DialogHeader>
@@ -277,7 +277,7 @@ export function AppUpdateNotification() {
{t("dialogs:appUpdate.readOnlyVolumeTitle", "Cannot install from disk image")}
</p>
<p className="text-muted-foreground">
{t("dialogs:appUpdate.readOnlyVolumeDescription", "Please move Auto Claude to your Applications folder before updating.")}
{t("dialogs:appUpdate.readOnlyVolumeDescription", "Please move Aperant to your Applications folder before updating.")}
</p>
</div>
</div>
@@ -76,7 +76,7 @@ export function AuthFailureModal({ onOpenSettings }: AuthFailureModalProps) {
{failureMessage}
</p>
<p className="text-sm text-muted-foreground">
{t('auth.failure.description', 'Please re-authenticate your Claude profile to continue using Auto Claude.')}
{t('auth.failure.description', 'Please re-authenticate your Claude profile to continue using Aperant.')}
</p>
{authFailureInfo.taskId && (
@@ -744,7 +744,7 @@ export function GitHubSetupModal({
Select Base Branch
</DialogTitle>
<DialogDescription>
Choose which branch Auto Claude should use as the base for creating task branches.
Choose which branch Aperant should use as the base for creating task branches.
</DialogDescription>
</DialogHeader>
@@ -811,7 +811,7 @@ export function GitHubSetupModal({
<div className="text-xs text-muted-foreground">
<p className="font-medium text-foreground">Why select a branch?</p>
<p className="mt-1">
Auto Claude creates isolated workspaces for each task. Selecting the right base branch ensures
Aperant creates isolated workspaces for each task. Selecting the right base branch ensures
your tasks start with the latest code from your main development line.
</p>
</div>
@@ -857,7 +857,7 @@ export function GitHubSetupModal({
<CheckCircle2 className="h-8 w-8 text-success" />
</div>
<p className="text-sm text-muted-foreground text-center">
Auto Claude is ready to use! You can now create tasks that will be
Aperant is ready to use! You can now create tasks that will be
automatically based on <code className="px-1 bg-muted rounded">{selectedBranch}</code>.
</p>
</div>
@@ -33,11 +33,15 @@ export function ProjectTabBar({
// Keyboard shortcuts for tab navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Skip if in input fields
// Skip if in input fields (but NOT xterm's hidden textarea —
// xterm already passes through Cmd/Ctrl+1-9 via attachCustomKeyEventHandler)
const target = e.target as HTMLElement;
const isXtermTextarea = target.classList?.contains('xterm-helper-textarea');
if (
e.target instanceof HTMLInputElement ||
!isXtermTextarea &&
(e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
(e.target as HTMLElement)?.isContentEditable
target?.isContentEditable)
) {
return;
}
@@ -480,7 +480,7 @@ export function Worktrees({ projectId }: WorktreesProps) {
Worktrees
</h2>
<p className="text-sm text-muted-foreground mt-1">
Manage isolated workspaces for your Auto Claude tasks
Manage isolated workspaces for your Aperant tasks
</p>
</div>
<div className="flex items-center gap-2">
@@ -569,7 +569,7 @@ export function Worktrees({ projectId }: WorktreesProps) {
</div>
<h3 className="text-lg font-semibold text-foreground">No Worktrees</h3>
<p className="text-sm text-muted-foreground mt-2 max-w-md">
Worktrees are created automatically when Auto Claude builds features.
Worktrees are created automatically when Aperant builds features.
You can also create terminal worktrees from the Agent Terminals tab.
</p>
</div>
@@ -921,7 +921,7 @@ export function PRDetail({
try {
// Auto-assign current user (you can get from GitHub config)
// For now, we'll just post the comment
const approvalMessage = `## ✅ Auto Claude PR Review - APPROVED\n\n${reviewResult.summary}\n\n---\n*This approval was generated by Auto Claude.*`;
const approvalMessage = `## ✅ Aperant PR Review - APPROVED\n\n${reviewResult.summary}\n\n---\n*This approval was generated by Aperant.*`;
await Promise.resolve(onPostComment(approvalMessage));
} finally {
// Clear loading state if PR hasn't changed
@@ -395,7 +395,7 @@ describe('PRDetail Clean Review Functionality', () => {
summary: 'All code passes review. No issues found.'
});
const cleanReviewMessage = `## ✅ Auto Claude PR Review - PASSED
const cleanReviewMessage = `## ✅ Aperant PR Review - PASSED
**Status:** All code is good
@@ -403,12 +403,12 @@ ${reviewResult.summary}
---
*This automated review found no issues. Generated by Auto Claude.*`;
*This automated review found no issues. Generated by Aperant.*`;
expect(cleanReviewMessage).toContain('## ✅ Auto Claude PR Review - PASSED');
expect(cleanReviewMessage).toContain('## ✅ Aperant PR Review - PASSED');
expect(cleanReviewMessage).toContain('**Status:** All code is good');
expect(cleanReviewMessage).toContain(reviewResult.summary);
expect(cleanReviewMessage).toContain('*This automated review found no issues. Generated by Auto Claude.*');
expect(cleanReviewMessage).toContain('*This automated review found no issues. Generated by Aperant.*');
});
it('should include custom summary in clean review comment', () => {
@@ -417,7 +417,7 @@ ${reviewResult.summary}
summary: customSummary
});
const cleanReviewMessage = `## ✅ Auto Claude PR Review - PASSED
const cleanReviewMessage = `## ✅ Aperant PR Review - PASSED
**Status:** All code is good
@@ -425,7 +425,7 @@ ${reviewResult.summary}
---
*This automated review found no issues. Generated by Auto Claude.*`;
*This automated review found no issues. Generated by Aperant.*`;
expect(cleanReviewMessage).toContain(customSummary);
});
@@ -435,7 +435,7 @@ ${reviewResult.summary}
summary: ''
});
const cleanReviewMessage = `## ✅ Auto Claude PR Review - PASSED
const cleanReviewMessage = `## ✅ Aperant PR Review - PASSED
**Status:** All code is good
@@ -443,7 +443,7 @@ ${reviewResult.summary}
---
*This automated review found no issues. Generated by Auto Claude.*`;
*This automated review found no issues. Generated by Aperant.*`;
expect(cleanReviewMessage).toBeDefined();
expect(cleanReviewMessage).toContain('All code is good');
@@ -174,7 +174,7 @@ export function ClaudeCodeStep({ onNext, onBack, onSkip }: ClaudeCodeStepProps)
{t('claudeCode.info.title', 'What is Claude Code?')}
</p>
<p className="text-sm text-muted-foreground">
{t('claudeCode.info.description', "Claude Code is Anthropic's official CLI that powers Auto Claude's AI features. It provides secure authentication and direct access to Claude models.")}
{t('claudeCode.info.description', "Claude Code is Anthropic's official CLI that powers Aperant's AI features. It provides secure authentication and direct access to Claude models.")}
</p>
</div>
</div>
@@ -97,7 +97,7 @@ export function FirstSpecStep({ onNext, onBack, onSkip, onOpenTaskCreator }: Fir
Create Your First Task
</h1>
<p className="mt-2 text-muted-foreground">
Describe what you want to build and let Auto Claude handle the rest
Describe what you want to build and let Aperant handle the rest
</p>
</div>
@@ -757,7 +757,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
Graphiti configured successfully
</h3>
<p className="mt-1 text-sm text-success/80">
Memory features are enabled. Auto Claude will maintain context
Memory features are enabled. Aperant will maintain context
across sessions for improved code understanding.
</p>
</div>
@@ -823,7 +823,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
What is Graphiti?
</p>
<p className="text-sm text-muted-foreground">
Graphiti is an intelligent memory layer that helps Auto Claude remember
Graphiti is an intelligent memory layer that helps Aperant remember
context across sessions. It uses a knowledge graph to store discoveries,
patterns, and insights about your codebase.
</p>
@@ -21,11 +21,11 @@ vi.mock('react-i18next', () => ({
// Return the key itself or provide specific translations
// Keys are without namespace since component uses useTranslation('namespace')
const translations: Record<string, string> = {
'welcome.title': 'Welcome to Auto Claude',
'welcome.title': 'Welcome to Aperant',
'welcome.subtitle': 'AI-powered autonomous coding assistant',
'welcome.getStarted': 'Get Started',
'welcome.skip': 'Skip Setup',
'wizard.helpText': 'Let us help you get started with Auto Claude',
'wizard.helpText': 'Let us help you get started with Aperant',
'welcome.features.aiPowered.title': 'AI-Powered',
'welcome.features.aiPowered.description': 'Powered by Claude',
'welcome.features.specDriven.title': 'Spec-Driven',
@@ -107,7 +107,7 @@ describe('OnboardingWizard Integration Tests', () => {
render(<OnboardingWizard {...defaultProps} />);
// Start at welcome step
expect(screen.getByText(/Welcome to Auto Claude/)).toBeInTheDocument();
expect(screen.getByText(/Welcome to Aperant/)).toBeInTheDocument();
// Click "Get Started" to go to accounts
const getStartedButton = screen.getByRole('button', { name: /Get Started/ });
@@ -147,7 +147,7 @@ describe('OnboardingWizard Integration Tests', () => {
// Should be back at welcome
await waitFor(() => {
expect(screen.getByText(/Welcome to Auto Claude/)).toBeInTheDocument();
expect(screen.getByText(/Welcome to Aperant/)).toBeInTheDocument();
});
});
});
@@ -157,25 +157,25 @@ describe('OnboardingWizard Integration Tests', () => {
render(<OnboardingWizard {...defaultProps} open={true} />);
// Wizard should be visible
expect(screen.getByText(/Welcome to Auto Claude/)).toBeInTheDocument();
expect(screen.getByText(/Welcome to Aperant/)).toBeInTheDocument();
});
it('should not show wizard when open is false', () => {
const { rerender } = render(<OnboardingWizard {...defaultProps} open={true} />);
expect(screen.getByText(/Welcome to Auto Claude/)).toBeInTheDocument();
expect(screen.getByText(/Welcome to Aperant/)).toBeInTheDocument();
// Close wizard
rerender(<OnboardingWizard {...defaultProps} open={false} />);
// Wizard content should not be visible
expect(screen.queryByText(/Welcome to Auto Claude/)).not.toBeInTheDocument();
expect(screen.queryByText(/Welcome to Aperant/)).not.toBeInTheDocument();
});
it('should not show wizard for users with existing auth', () => {
render(<OnboardingWizard {...defaultProps} open={false} />);
expect(screen.queryByText(/Welcome to Auto Claude/)).not.toBeInTheDocument();
expect(screen.queryByText(/Welcome to Aperant/)).not.toBeInTheDocument();
});
});
@@ -110,6 +110,20 @@ export function AddAccountDialog({
}
}, [open, editAccount, provider, billingModelOverride]);
// Parse DUPLICATE_EMAIL error from backend and show user-friendly toast
const handleDuplicateEmailError = useCallback((error: string): boolean => {
if (error.startsWith('DUPLICATE_EMAIL:')) {
const existingName = error.slice('DUPLICATE_EMAIL:'.length);
toast({
variant: 'destructive',
title: t('providers.dialog.toast.error'),
description: t('providers.dialog.toast.duplicateEmail', { existingName }),
});
return true;
}
return false;
}, [toast, t]);
const isOAuthOnly = (provider === 'anthropic' || provider === 'openai') && authType === 'oauth';
const isCodexOAuth = provider === 'openai' && authType === 'oauth';
@@ -190,10 +204,16 @@ export function AddAccountDialog({
: t('providers.dialog.toast.added'),
description: name.trim(),
});
} else if (result.error && !handleDuplicateEmailError(result.error)) {
toast({
variant: 'destructive',
title: t('providers.dialog.toast.error'),
description: result.error,
});
}
};
autoSave();
}, [oauthStatus, isCodexOAuth, accountSaved, name, provider, oauthProfileId, isEditing, editAccount, oauthEmail, addProviderAccount, updateProviderAccount, toast, t, refreshUsageData]);
}, [oauthStatus, isCodexOAuth, accountSaved, name, provider, oauthProfileId, isEditing, editAccount, oauthEmail, addProviderAccount, updateProviderAccount, handleDuplicateEmailError, toast, t, refreshUsageData]);
const canSave = () => {
if (!name.trim()) return false;
@@ -264,8 +284,14 @@ export function AddAccountDialog({
description: name.trim(),
});
await refreshUsageData();
onOpenChange(false);
} else if (saveResult.error && !handleDuplicateEmailError(saveResult.error)) {
toast({
variant: 'destructive',
title: t('providers.dialog.toast.error'),
description: saveResult.error,
});
}
onOpenChange(false);
}, 800);
} else {
setOauthStatus('error');
@@ -318,7 +344,7 @@ export function AddAccountDialog({
setOauthStatus('error');
setOauthError(err instanceof Error ? err.message : 'Unexpected error');
}
}, [name, t, toast, isCodexOAuth, isEditing, editAccount, provider, addProviderAccount, updateProviderAccount, onOpenChange, refreshUsageData]);
}, [name, t, toast, isCodexOAuth, isEditing, editAccount, provider, addProviderAccount, updateProviderAccount, handleDuplicateEmailError, onOpenChange, refreshUsageData]);
const handleFallbackTerminal = useCallback(async () => {
if (!name.trim()) {
@@ -342,7 +368,7 @@ export function AddAccountDialog({
createdAt: new Date(),
});
if (!profileResult.success || !profileResult.data) {
toast({ variant: 'destructive', title: 'Failed to create profile' });
toast({ variant: 'destructive', title: t('providers.dialog.toast.createProfileFailed') });
return;
}
profileId = profileResult.data.id;
@@ -352,7 +378,7 @@ export function AddAccountDialog({
// Get terminal config for embedded AuthTerminal
const authResult = await window.electronAPI.authenticateClaudeProfile(profileId);
if (!authResult.success || !authResult.data) {
toast({ variant: 'destructive', title: authResult.error ?? 'Failed to prepare terminal' });
toast({ variant: 'destructive', title: authResult.error ?? t('providers.dialog.toast.authPrepareFailed') });
return;
}
@@ -362,7 +388,7 @@ export function AddAccountDialog({
} catch (err) {
toast({
variant: 'destructive',
title: err instanceof Error ? err.message : 'Unexpected error',
title: err instanceof Error ? err.message : t('providers.dialog.toast.unexpectedError'),
});
}
}, [name, oauthProfileId, t, toast]);
@@ -421,7 +447,7 @@ export function AddAccountDialog({
description: name.trim(),
});
onOpenChange(false);
} else {
} else if (result.error && !handleDuplicateEmailError(result.error)) {
toast({
variant: 'destructive',
title: t('providers.dialog.toast.error'),
@@ -51,9 +51,8 @@ export function AuthTerminal({
const loginSentRef = useRef(false); // Track if /login was already sent
const loginTimeoutRef = useRef<NodeJS.Timeout | null>(null); // Track setTimeout for cleanup
const successTimeoutRef = useRef<NodeJS.Timeout | null>(null); // Track success auto-close timeout for cleanup
const authCompletedRef = useRef(false); // Track if auth has already completed to prevent race conditions
const [status, setStatus] = useState<'connecting' | 'ready' | 'onboarding' | 'success' | 'error'>('connecting');
const [status, setStatus] = useState<'connecting' | 'ready' | 'success' | 'error'>('connecting');
const [authEmail, setAuthEmail] = useState<string | undefined>();
const [errorMessage, setErrorMessage] = useState<string | undefined>();
@@ -235,7 +234,6 @@ export function AuthTerminal({
thisTerminalId: terminalId,
isMatch: info.terminalId === terminalId,
success: info.success,
needsOnboarding: info.needsOnboarding,
email: info.email,
currentStatus: statusRef.current,
loginSent: loginSentRef.current
@@ -243,16 +241,9 @@ export function AuthTerminal({
if (info.terminalId === terminalId) {
if (info.success) {
setAuthEmail(info.email);
// If needsOnboarding is true, user should complete setup in terminal
// Otherwise, authentication is fully complete
if (info.needsOnboarding) {
debugLog('Setting status to onboarding', { terminalId });
setStatus('onboarding');
} else {
debugLog('Setting status to success (no onboarding needed)', { terminalId });
setStatus('success');
onAuthSuccess?.(info.email);
}
debugLog('Setting status to success', { terminalId });
setStatus('success');
onAuthSuccess?.(info.email);
} else {
debugLog('OAuth failed', { terminalId, message: info.message });
setStatus('error');
@@ -272,60 +263,13 @@ export function AuthTerminal({
terminalId,
exitCode,
currentStatus: statusRef.current,
loginSent: loginSentRef.current,
willTransitionToSuccess: statusRef.current === 'onboarding' && exitCode === 0
loginSent: loginSentRef.current
});
// If we were in onboarding status and terminal exits with code 0,
// that means the user completed the onboarding successfully
if (statusRef.current === 'onboarding' && exitCode === 0) {
// Prevent race condition with onboarding-complete handler
if (authCompletedRef.current) {
debugLog('SKIPPED exit handler - auth already completed', { terminalId });
return;
}
authCompletedRef.current = true;
debugLog('Transitioning from onboarding to success', { terminalId });
setStatus('success');
onAuthSuccess?.(authEmailRef.current);
}
// Don't close automatically - let user see any error messages
}
});
cleanupFnsRef.current.push(unsubExit);
// Handle onboarding complete (Claude shows ready state after login)
const unsubOnboardingComplete = window.electronAPI.onTerminalOnboardingComplete((info) => {
if (info.terminalId === terminalId) {
console.warn('[AuthTerminal] Onboarding complete:', info);
debugLog('Onboarding complete event', {
terminalId: info.terminalId,
profileId: info.profileId,
currentStatus: statusRef.current
});
// Only process if we're in onboarding status
if (statusRef.current === 'onboarding') {
// Prevent race condition with terminal exit handler
if (authCompletedRef.current) {
debugLog('SKIPPED onboarding-complete handler - auth already completed', { terminalId });
return;
}
authCompletedRef.current = true;
debugLog('Auto-closing terminal after onboarding complete', { terminalId });
setStatus('success');
onAuthSuccess?.(authEmailRef.current);
// Auto-close after a brief delay to show success UI
successTimeoutRef.current = setTimeout(() => {
if (isCreatedRef.current) {
window.electronAPI.destroyTerminal(terminalId).catch(console.error);
isCreatedRef.current = false;
}
onClose();
}, 1500);
}
}
});
cleanupFnsRef.current.push(unsubOnboardingComplete);
return () => {
debugLog('Cleaning up event listeners', { terminalId });
inputDisposable.dispose();
@@ -408,9 +352,6 @@ export function AuthTerminal({
{status === 'ready' && (
<div className="h-2 w-2 rounded-full bg-yellow-500 animate-pulse" />
)}
{status === 'onboarding' && (
<div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
)}
{status === 'success' && (
<CheckCircle2 className="h-4 w-4 text-success" />
)}
@@ -420,7 +361,6 @@ export function AuthTerminal({
<span className="text-sm font-medium">
{status === 'connecting' && t('authTerminal.connecting')}
{status === 'ready' && t('authTerminal.authenticate', { profileName })}
{status === 'onboarding' && t('authTerminal.completeSetup', { profileName })}
{status === 'success' && (authEmail ? t('authTerminal.authenticatedAs', { email: authEmail }) : t('authTerminal.authenticated'))}
{status === 'error' && t('authTerminal.authError')}
</span>
@@ -446,13 +386,6 @@ export function AuthTerminal({
/>
{/* Status bar */}
{status === 'onboarding' && (
<div className="px-3 py-2 border-t border-border bg-blue-500/10">
<p className="text-sm text-blue-600 dark:text-blue-400">
{t('authTerminal.onboardingMessage')}
</p>
</div>
)}
{status === 'success' && (
<div className="px-3 py-2 border-t border-border bg-success/10">
<p className="text-sm text-success">
@@ -96,7 +96,7 @@ export function DebugSettings() {
{t('debug.errorReporting.label', 'Anonymous Error Reporting')}
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
{t('debug.errorReporting.description', 'Send crash reports to help improve Auto Claude. No personal data or code is collected.')}
{t('debug.errorReporting.description', 'Send crash reports to help improve Aperant. No personal data or code is collected.')}
</p>
</div>
</div>
@@ -320,7 +320,7 @@ export function DevToolsSettings({ settings, onSettingsChange }: DevToolsSetting
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t('devtools.ide.description', 'Auto Claude will open worktrees in this editor')}
{t('devtools.ide.description', 'Aperant will open worktrees in this editor')}
</p>
{/* Custom IDE Path */}
@@ -382,7 +382,7 @@ export function DevToolsSettings({ settings, onSettingsChange }: DevToolsSetting
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t('devtools.terminal.description', 'Auto Claude will open terminal sessions here')}
{t('devtools.terminal.description', 'Aperant will open terminal sessions here')}
</p>
{/* Custom Terminal Path */}
@@ -104,7 +104,6 @@ export const terminalMock = {
onTerminalAuthCreated: () => () => {},
onTerminalClaudeBusy: () => () => {},
onTerminalClaudeExit: () => () => {},
onTerminalOnboardingComplete: () => () => {},
onTerminalPendingResume: () => () => {},
onTerminalProfileChanged: () => () => {},
onTerminalOAuthCodeNeeded: () => () => {},
-1
View File
@@ -109,7 +109,6 @@ export const IPC_CHANNELS = {
TERMINAL_OAUTH_CODE_SUBMIT: 'terminal:oauthCodeSubmit', // User submitted OAuth code to send to terminal
TERMINAL_CLAUDE_BUSY: 'terminal:claudeBusy', // Claude Code busy state (for visual indicator)
TERMINAL_CLAUDE_EXIT: 'terminal:claudeExit', // Claude Code exited (returned to shell)
TERMINAL_ONBOARDING_COMPLETE: 'terminal:onboardingComplete', // Claude onboarding complete (ready for input after login)
TERMINAL_PROFILE_CHANGED: 'terminal:profileChanged', // Profile changed, terminals need refresh (main -> renderer)
// Claude profile management (multi-account support)
@@ -71,6 +71,12 @@ export const ALL_AVAILABLE_MODELS: ModelOption[] = [
{ value: 'glm-4.7', label: 'GLM-4.7', provider: 'zai', description: 'Previous flagship', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
{ value: 'glm-4.6v', label: 'GLM-4.6V', provider: 'zai', description: 'Multimodal', capabilities: { thinking: false, tools: true, vision: true, contextWindow: 128000 } },
{ value: 'glm-4.5-flash', label: 'GLM-4.5 Flash', provider: 'zai', description: 'Fast', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
// Mascarade (LLM Orchestration Engine)
{ value: 'mascarade-router', label: 'Mascarade Router', provider: 'mascarade', description: 'Multi-LLM routing (best/cheapest/fastest)', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
{ value: 'mascarade-writer', label: 'Mascarade Writer', provider: 'mascarade', description: 'Content generation agent', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
{ value: 'mascarade-coder', label: 'Mascarade Coder', provider: 'mascarade', description: 'Code review & generation', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
{ value: 'mascarade-analyst', label: 'Mascarade Analyst', provider: 'mascarade', description: 'Data analysis agent', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
{ value: 'mascarade-planner', label: 'Mascarade Planner', provider: 'mascarade', description: 'Project decomposition', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
];
// Maps model shorthand to actual Claude model IDs
@@ -322,6 +328,12 @@ export const PROVIDER_PRESET_DEFINITIONS: Partial<Record<BuiltinProvider, Record
balanced: { primaryModel: '', primaryThinking: 'low', phaseModels: { spec: '', planning: '', coding: '', qa: '' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
quick: { primaryModel: '', primaryThinking: 'low', phaseModels: { spec: '', planning: '', coding: '', qa: '' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
},
mascarade: {
auto: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
complex: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
balanced: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
quick: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
},
};
/**
@@ -73,4 +73,11 @@ export const PROVIDER_REGISTRY: ProviderInfo[] = [
authMethods: ['api-key'], envVars: [],
configFields: ['baseUrl'],
},
{
id: 'mascarade', name: 'Mascarade', description: 'LLM Orchestration Engine — multi-provider routing with 19+ agents',
category: 'local',
authMethods: ['api-key'], envVars: ['MASCARADE_API_KEY'],
configFields: ['baseUrl'],
website: 'https://mascarade.saillant.cc',
},
];
@@ -402,17 +402,17 @@
"postCleanReview": "Post Clean Review",
"postingCleanReview": "Posting...",
"cleanReviewPosted": "Clean review posted",
"cleanReviewMessageTitle": "## ✅ Auto Claude PR Review - PASSED",
"cleanReviewMessageTitle": "## ✅ Aperant PR Review - PASSED",
"cleanReviewMessageStatus": "**Status:** All code is good",
"cleanReviewMessageFooter": "*This automated review found no issues. Generated by Auto Claude.*",
"cleanReviewMessageFooter": "*This automated review found no issues. Generated by Aperant.*",
"failedPostCleanReview": "Failed to post clean review",
"viewErrorDetails": "View details",
"hideErrorDetails": "Hide details",
"postBlockedStatus": "Post Status",
"postingBlockedStatus": "Posting...",
"blockedStatusPosted": "Status posted to PR",
"blockedStatusMessageTitle": "## 🤖 Auto Claude PR Review",
"blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Auto Claude.*",
"blockedStatusMessageTitle": "## 🤖 Aperant PR Review",
"blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Aperant.*",
"failedPostBlockedStatus": "Failed to post status",
"branchSynced": "Branch synced ({{count}} commit from base)",
"branchSynced_plural": "Branch synced ({{count}} commits from base)",
@@ -606,7 +606,7 @@
"codeSubmitFailed": "Failed to Submit Code",
"codeSubmitFailedDescription": "Please try again or copy the code manually to the terminal.",
"authenticateTitle": "Authenticate with Claude",
"authenticateDescription": "Auto Claude requires Claude AI authentication for AI-powered features like Roadmap generation, Task automation, and Ideation.",
"authenticateDescription": "Aperant requires Claude AI authentication for AI-powered features like Roadmap generation, Task automation, and Ideation.",
"authenticateTerminalInfo": "This will open a terminal with Claude CLI where you can authenticate. Your credentials are stored securely and are valid for 1 year.",
"completeAuthTitle": "Complete Authentication",
"terminalOpened": "A terminal window has opened with Claude CLI.",
@@ -621,7 +621,7 @@
"successTitle": "Successfully Authenticated!",
"connectedAs": "Connected as {{email}}",
"credentialsSaved": "Your Claude credentials have been saved",
"canUseFeatures": "You can now use all Auto Claude AI features",
"canUseFeatures": "You can now use all Aperant AI features",
"authFailed": "Authentication Failed",
"skipForNow": "Skip for now",
"manualTokenEntry": "Manual Token Entry",
@@ -674,11 +674,9 @@
"authFailed": "Authentication failed",
"connecting": "Connecting...",
"authenticate": "Authenticate: {{profileName}}",
"completeSetup": "Complete setup: {{profileName}}",
"authenticatedAs": "Authenticated as {{email}}",
"authenticated": "Authenticated!",
"authError": "Authentication Error",
"onboardingMessage": "Token received! Complete the Claude Code setup below - this window will close automatically when done.",
"successMessage": "Authentication successful! Closing..."
},
"profileCreated": {
@@ -749,7 +747,7 @@
"tokenInvalid": "Your authentication token is invalid.",
"tokenMissing": "No authentication token found.",
"authFailed": "Authentication failed.",
"description": "Please re-authenticate your Claude profile to continue using Auto Claude.",
"description": "Please re-authenticate your Claude profile to continue using Aperant.",
"taskAffected": "Task affected",
"technicalDetails": "Technical details",
"goToSettings": "Go to Settings"
@@ -1,23 +1,23 @@
{
"initialize": {
"title": "Initialize Auto Claude",
"description": "This project doesn't have Auto Claude initialized. Would you like to set it up now?",
"title": "Initialize Aperant",
"description": "This project doesn't have Aperant initialized. Would you like to set it up now?",
"willDo": "This will:",
"createFolder": "Create a .auto-claude folder in your project",
"copyFramework": "Copy the Auto Claude framework files",
"copyFramework": "Copy the Aperant framework files",
"setupSpecs": "Set up the specs directory for your tasks",
"sourcePathNotConfigured": "Source path not configured",
"sourcePathNotConfiguredDescription": "Please set the Auto Claude source path in App Settings before initializing.",
"sourcePathNotConfiguredDescription": "Please set the Aperant source path in App Settings before initializing.",
"initFailed": "Initialization Failed",
"initFailedDescription": "Failed to initialize Auto Claude. Please try again."
"initFailedDescription": "Failed to initialize Aperant. Please try again."
},
"gitSetup": {
"title": "Git Repository Required",
"description": "Auto Claude uses git to safely build features in isolated workspaces",
"description": "Aperant uses git to safely build features in isolated workspaces",
"notGitRepo": "This folder is not a git repository",
"noCommits": "Git repository has no commits",
"needsInit": "Git needs to be initialized before Auto Claude can manage your code.",
"needsCommit": "At least one commit is required for Auto Claude to create worktrees.",
"needsInit": "Git needs to be initialized before Aperant can manage your code.",
"needsCommit": "At least one commit is required for Aperant to create worktrees.",
"willSetup": "We'll set up git for you:",
"initRepo": "Initialize a new git repository",
"createCommit": "Create an initial commit with your current files",
@@ -25,25 +25,25 @@
"settingUp": "Setting up Git",
"initializingRepo": "Initializing git repository and creating initial commit...",
"success": "Git Initialized",
"readyToUse": "Your project is now ready to use with Auto Claude!"
"readyToUse": "Your project is now ready to use with Aperant!"
},
"githubSetup": {
"connectTitle": "Connect to GitHub",
"connectDescription": "Auto Claude requires GitHub to manage your code branches and keep tasks up to date.",
"connectDescription": "Aperant requires GitHub to manage your code branches and keep tasks up to date.",
"claudeTitle": "Connect to Claude AI",
"claudeDescription": "Auto Claude uses Claude AI for intelligent features like Roadmap generation, Task automation, and Ideation.",
"claudeDescription": "Aperant uses Claude AI for intelligent features like Roadmap generation, Task automation, and Ideation.",
"aiProviderTitle": "Connect to AI",
"aiProviderDescription": "Add an AI provider account to power features like Roadmap generation, Task automation, and Ideation.",
"aiProviderReady": "You have at least one AI provider configured. You can continue to the next step.",
"skipForNow": "Skip for now",
"continue": "Continue",
"selectRepo": "Select Repository",
"repoDescription": "Auto Claude will use this repository for managing task branches and keeping your code up to date.",
"repoDescription": "Aperant will use this repository for managing task branches and keeping your code up to date.",
"selectBranch": "Select Base Branch",
"branchDescription": "Choose which branch Auto Claude should use as the base for creating task branches.",
"branchDescription": "Choose which branch Aperant should use as the base for creating task branches.",
"whyBranch": "Why select a branch?",
"branchExplanation": "Auto Claude creates isolated workspaces for each task. Selecting the right base branch ensures your tasks start with the latest code from your main development line.",
"ready": "Auto Claude is ready to use! You can now create tasks that will be automatically based on the {{branchName}} branch.",
"branchExplanation": "Aperant creates isolated workspaces for each task. Selecting the right base branch ensures your tasks start with the latest code from your main development line.",
"ready": "Aperant is ready to use! You can now create tasks that will be automatically based on the {{branchName}} branch.",
"createRepoAriaLabel": "Create new repository on GitHub",
"linkRepoAriaLabel": "Link to existing repository",
"goBackAriaLabel": "Go back to repository selection",
@@ -53,9 +53,9 @@
},
"worktrees": {
"title": "Worktrees",
"description": "Manage isolated workspaces for your Auto Claude tasks",
"description": "Manage isolated workspaces for your Aperant tasks",
"empty": "No Worktrees",
"emptyDescription": "Worktrees are created automatically when Auto Claude builds features. They provide isolated workspaces for each task.",
"emptyDescription": "Worktrees are created automatically when Aperant builds features. They provide isolated workspaces for each task.",
"merge": "Merge Worktree",
"mergeDescription": "Merge changes from this worktree into the base branch.",
"delete": "Delete Worktree?",
@@ -77,7 +77,7 @@
"errorDescription": "Failed to cleanup worktree. Please try again."
},
"update": {
"title": "Auto Claude",
"title": "Aperant",
"projectInitialized": "Project is initialized."
},
"addFeature": {
@@ -156,7 +156,7 @@
},
"appUpdate": {
"title": "App Update Available",
"description": "A new version of Auto Claude is ready to download",
"description": "A new version of Aperant is ready to download",
"newVersion": "New Version",
"released": "Released",
"downloading": "Downloading...",
@@ -169,7 +169,7 @@
"claudeCodeChangelog": "View Claude Code Changelog",
"claudeCodeChangelogAriaLabel": "View Claude Code Changelog (opens in new window)",
"readOnlyVolumeTitle": "Cannot install from disk image",
"readOnlyVolumeDescription": "Please move Auto Claude to your Applications folder before updating."
"readOnlyVolumeDescription": "Please move Aperant to your Applications folder before updating."
},
"addCompetitor": {
"title": "Add Competitor",
@@ -31,7 +31,7 @@
"help": "Help & Feedback"
},
"messages": {
"initializeToCreateTasks": "Initialize Auto Claude to create tasks"
"initializeToCreateTasks": "Initialize Aperant to create tasks"
},
"updateBanner": {
"title": "Update Available",
@@ -810,7 +810,11 @@
"toast": {
"added": "Account added",
"updated": "Account updated",
"error": "Failed to save account"
"error": "Failed to save account",
"duplicateEmail": "This email is already registered as \"{{existingName}}\"",
"createProfileFailed": "Failed to create profile",
"authPrepareFailed": "Failed to prepare terminal",
"unexpectedError": "Unexpected error"
}
},
"toast": {
@@ -918,7 +922,7 @@
"description": "Web browser automation for testing"
},
"autoClaude": {
"name": "Auto-Claude Tools",
"name": "Aperant Tools",
"description": "Build progress tracking"
}
},
@@ -1,6 +1,6 @@
{
"hero": {
"title": "Welcome to Auto Claude",
"title": "Welcome to Aperant",
"subtitle": "Build software autonomously with AI-powered agents"
},
"actions": {
@@ -411,17 +411,17 @@
"postCleanReview": "Publier révision propre",
"postingCleanReview": "Publication...",
"cleanReviewPosted": "Révision propre publiée",
"cleanReviewMessageTitle": "## ✅ Auto Claude PR Review - PASSED",
"cleanReviewMessageTitle": "## ✅ Aperant PR Review - PASSED",
"cleanReviewMessageStatus": "**Status:** All code is good",
"cleanReviewMessageFooter": "*This automated review found no issues. Generated by Auto Claude.*",
"cleanReviewMessageFooter": "*This automated review found no issues. Generated by Aperant.*",
"failedPostCleanReview": "Échec de la publication de la révision",
"viewErrorDetails": "Voir les détails",
"hideErrorDetails": "Masquer les détails",
"postBlockedStatus": "Publier le statut",
"postingBlockedStatus": "Publication...",
"blockedStatusPosted": "Statut publié sur la PR",
"blockedStatusMessageTitle": "## 🤖 Auto Claude PR Review",
"blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Auto Claude.*",
"blockedStatusMessageTitle": "## 🤖 Aperant PR Review",
"blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Aperant.*",
"failedPostBlockedStatus": "Échec de la publication du statut",
"disputed": "Contesté",
"disputedByValidator": "Contesté par le validateur ({{count}})",
@@ -606,7 +606,7 @@
"codeSubmitFailed": "Échec de la soumission du code",
"codeSubmitFailedDescription": "Veuillez réessayer ou copier le code manuellement dans le terminal.",
"authenticateTitle": "S'authentifier avec Claude",
"authenticateDescription": "Auto Claude nécessite l'authentification Claude AI pour les fonctionnalités basées sur l'IA comme la génération de feuille de route, l'automatisation des tâches et l'idéation.",
"authenticateDescription": "Aperant nécessite l'authentification Claude AI pour les fonctionnalités basées sur l'IA comme la génération de feuille de route, l'automatisation des tâches et l'idéation.",
"authenticateTerminalInfo": "Cela ouvrira un terminal avec Claude CLI où vous pourrez vous authentifier. Vos identifiants sont stockés de manière sécurisée et sont valides pendant 1 an.",
"completeAuthTitle": "Terminer l'authentification",
"terminalOpened": "Une fenêtre de terminal s'est ouverte avec Claude CLI.",
@@ -621,7 +621,7 @@
"successTitle": "Authentification réussie !",
"connectedAs": "Connecté en tant que {{email}}",
"credentialsSaved": "Vos identifiants Claude ont été sauvegardés",
"canUseFeatures": "Vous pouvez maintenant utiliser toutes les fonctionnalités IA d'Auto Claude",
"canUseFeatures": "Vous pouvez maintenant utiliser toutes les fonctionnalités IA d'Aperant",
"authFailed": "Échec de l'authentification",
"skipForNow": "Passer pour l'instant",
"manualTokenEntry": "Saisie manuelle du jeton",
@@ -674,11 +674,9 @@
"authFailed": "Échec de l'authentification",
"connecting": "Connexion...",
"authenticate": "Authentifier : {{profileName}}",
"completeSetup": "Terminer la configuration : {{profileName}}",
"authenticatedAs": "Authentifié en tant que {{email}}",
"authenticated": "Authentifié !",
"authError": "Erreur d'authentification",
"onboardingMessage": "Jeton reçu ! Terminez la configuration de Claude Code ci-dessous - cette fenêtre se fermera automatiquement une fois terminé.",
"successMessage": "Authentification réussie ! Fermeture..."
},
"profileCreated": {
@@ -749,7 +747,7 @@
"tokenInvalid": "Votre jeton d'authentification est invalide.",
"tokenMissing": "Aucun jeton d'authentification trouvé.",
"authFailed": "Échec de l'authentification.",
"description": "Veuillez vous ré-authentifier pour continuer à utiliser Auto Claude.",
"description": "Veuillez vous ré-authentifier pour continuer à utiliser Aperant.",
"taskAffected": "Tâche affectée",
"technicalDetails": "Détails techniques",
"goToSettings": "Aller aux paramètres"
@@ -1,23 +1,23 @@
{
"initialize": {
"title": "Initialiser Auto Claude",
"description": "Ce projet n'a pas Auto Claude initialisé. Voulez-vous le configurer maintenant ?",
"title": "Initialiser Aperant",
"description": "Ce projet n'a pas Aperant initialisé. Voulez-vous le configurer maintenant ?",
"willDo": "Ceci va :",
"createFolder": "Créer un dossier .auto-claude dans votre projet",
"copyFramework": "Copier les fichiers du framework Auto Claude",
"copyFramework": "Copier les fichiers du framework Aperant",
"setupSpecs": "Configurer le répertoire des spécifications pour vos tâches",
"sourcePathNotConfigured": "Chemin source non configuré",
"sourcePathNotConfiguredDescription": "Veuillez définir le chemin source Auto Claude dans les paramètres de l'application avant d'initialiser.",
"sourcePathNotConfiguredDescription": "Veuillez définir le chemin source Aperant dans les paramètres de l'application avant d'initialiser.",
"initFailed": "Échec de l'initialisation",
"initFailedDescription": "Échec de l'initialisation de Auto Claude. Veuillez réessayer."
"initFailedDescription": "Échec de l'initialisation de Aperant. Veuillez réessayer."
},
"gitSetup": {
"title": "Dépôt Git requis",
"description": "Auto Claude utilise git pour construire des fonctionnalités en toute sécurité dans des espaces de travail isolés",
"description": "Aperant utilise git pour construire des fonctionnalités en toute sécurité dans des espaces de travail isolés",
"notGitRepo": "Ce dossier n'est pas un dépôt git",
"noCommits": "Le dépôt git n'a pas de commits",
"needsInit": "Git doit être initialisé avant que Auto Claude puisse gérer votre code.",
"needsCommit": "Au moins un commit est requis pour que Auto Claude puisse créer des worktrees.",
"needsInit": "Git doit être initialisé avant que Aperant puisse gérer votre code.",
"needsCommit": "Au moins un commit est requis pour que Aperant puisse créer des worktrees.",
"willSetup": "Nous allons configurer git pour vous :",
"initRepo": "Initialiser un nouveau dépôt git",
"createCommit": "Créer un commit initial avec vos fichiers actuels",
@@ -25,25 +25,25 @@
"settingUp": "Configuration de Git",
"initializingRepo": "Initialisation du dépôt git et création du commit initial...",
"success": "Git initialisé",
"readyToUse": "Votre projet est maintenant prêt à être utilisé avec Auto Claude !"
"readyToUse": "Votre projet est maintenant prêt à être utilisé avec Aperant !"
},
"githubSetup": {
"connectTitle": "Connecter à GitHub",
"connectDescription": "Auto Claude nécessite GitHub pour gérer vos branches de code et maintenir les tâches à jour.",
"connectDescription": "Aperant nécessite GitHub pour gérer vos branches de code et maintenir les tâches à jour.",
"claudeTitle": "Connecter à Claude AI",
"claudeDescription": "Auto Claude utilise Claude AI pour des fonctionnalités intelligentes comme la génération de feuille de route, l'automatisation des tâches et l'idéation.",
"claudeDescription": "Aperant utilise Claude AI pour des fonctionnalités intelligentes comme la génération de feuille de route, l'automatisation des tâches et l'idéation.",
"aiProviderTitle": "Connecter à l'IA",
"aiProviderDescription": "Ajoutez un compte fournisseur IA pour activer des fonctionnalités comme la génération de feuille de route, l'automatisation des tâches et l'idéation.",
"aiProviderReady": "Vous avez au moins un fournisseur IA configuré. Vous pouvez passer à l'étape suivante.",
"skipForNow": "Passer pour l'instant",
"continue": "Continuer",
"selectRepo": "Sélectionner le dépôt",
"repoDescription": "Auto Claude utilisera ce dépôt pour gérer les branches de tâches et maintenir votre code à jour.",
"repoDescription": "Aperant utilisera ce dépôt pour gérer les branches de tâches et maintenir votre code à jour.",
"selectBranch": "Sélectionner la branche de base",
"branchDescription": "Choisissez quelle branche Auto Claude doit utiliser comme base pour créer les branches de tâches.",
"branchDescription": "Choisissez quelle branche Aperant doit utiliser comme base pour créer les branches de tâches.",
"whyBranch": "Pourquoi sélectionner une branche ?",
"branchExplanation": "Auto Claude crée des espaces de travail isolés pour chaque tâche. Sélectionner la bonne branche de base garantit que vos tâches démarrent avec le code le plus récent de votre ligne de développement principale.",
"ready": "Auto Claude est prêt à l'emploi ! Vous pouvez maintenant créer des tâches qui seront automatiquement basées sur la branche {{branchName}}.",
"branchExplanation": "Aperant crée des espaces de travail isolés pour chaque tâche. Sélectionner la bonne branche de base garantit que vos tâches démarrent avec le code le plus récent de votre ligne de développement principale.",
"ready": "Aperant est prêt à l'emploi ! Vous pouvez maintenant créer des tâches qui seront automatiquement basées sur la branche {{branchName}}.",
"createRepoAriaLabel": "Créer un nouveau dépôt sur GitHub",
"linkRepoAriaLabel": "Lier à un dépôt existant",
"goBackAriaLabel": "Retourner à la sélection du dépôt",
@@ -53,9 +53,9 @@
},
"worktrees": {
"title": "Worktrees",
"description": "Gérez les espaces de travail isolés pour vos tâches Auto Claude",
"description": "Gérez les espaces de travail isolés pour vos tâches Aperant",
"empty": "Aucun worktree",
"emptyDescription": "Les worktrees sont créés automatiquement quand Auto Claude construit des fonctionnalités. Ils fournissent des espaces de travail isolés pour chaque tâche.",
"emptyDescription": "Les worktrees sont créés automatiquement quand Aperant construit des fonctionnalités. Ils fournissent des espaces de travail isolés pour chaque tâche.",
"merge": "Fusionner le worktree",
"mergeDescription": "Fusionner les modifications de ce worktree dans la branche de base.",
"delete": "Supprimer le worktree ?",
@@ -77,7 +77,7 @@
"errorDescription": "Échec du nettoyage du worktree. Veuillez réessayer."
},
"update": {
"title": "Auto Claude",
"title": "Aperant",
"projectInitialized": "Le projet est initialisé."
},
"addFeature": {
@@ -156,7 +156,7 @@
},
"appUpdate": {
"title": "Mise à jour de l'application disponible",
"description": "Une nouvelle version d'Auto Claude est prête à être téléchargée",
"description": "Une nouvelle version d'Aperant est prête à être téléchargée",
"newVersion": "Nouvelle version",
"released": "Publiée",
"downloading": "Téléchargement...",
@@ -169,7 +169,7 @@
"claudeCodeChangelog": "Voir le journal des modifications Claude Code",
"claudeCodeChangelogAriaLabel": "Voir le journal des modifications Claude Code (s'ouvre dans une nouvelle fenêtre)",
"readOnlyVolumeTitle": "Impossible d'installer depuis une image disque",
"readOnlyVolumeDescription": "Veuillez déplacer Auto Claude dans votre dossier Applications avant de mettre à jour."
"readOnlyVolumeDescription": "Veuillez déplacer Aperant dans votre dossier Applications avant de mettre à jour."
},
"addCompetitor": {
"title": "Ajouter un concurrent",
@@ -31,7 +31,7 @@
"help": "Aide & Feedback"
},
"messages": {
"initializeToCreateTasks": "Initialisez Auto Claude pour créer des tâches"
"initializeToCreateTasks": "Initialisez Aperant pour créer des tâches"
},
"updateBanner": {
"title": "Mise à jour disponible",
@@ -810,7 +810,11 @@
"toast": {
"added": "Compte ajouté",
"updated": "Compte mis à jour",
"error": "Échec de l'enregistrement du compte"
"error": "Échec de l'enregistrement du compte",
"duplicateEmail": "Cet e-mail est déjà enregistré sous \"{{existingName}}\"",
"createProfileFailed": "Échec de la création du profil",
"authPrepareFailed": "Échec de la préparation du terminal",
"unexpectedError": "Erreur inattendue"
}
},
"toast": {
@@ -918,7 +922,7 @@
"description": "Automatisation du navigateur web pour les tests"
},
"autoClaude": {
"name": "Outils Auto-Claude",
"name": "Outils Aperant",
"description": "Suivi de la progression du build"
}
},
@@ -1,6 +1,6 @@
{
"hero": {
"title": "Bienvenue sur Auto Claude",
"title": "Bienvenue sur Aperant",
"subtitle": "Construisez des logiciels de manière autonome avec des agents IA"
},
"actions": {
-8
View File
@@ -287,8 +287,6 @@ export interface ElectronAPI {
success: boolean;
message?: string;
detectedAt: string;
/** If true, user should complete onboarding in terminal before closing */
needsOnboarding?: boolean;
}) => void) => () => void;
/** Listen for auth terminal creation - allows UI to display the OAuth terminal */
onTerminalAuthCreated: (callback: (info: {
@@ -300,12 +298,6 @@ export interface ElectronAPI {
onTerminalClaudeBusy: (callback: (id: string, isBusy: boolean) => void) => () => void;
/** Listen for Claude exit (user closed Claude within terminal, returned to shell) */
onTerminalClaudeExit: (callback: (id: string) => void) => () => void;
/** Listen for onboarding complete (Claude shows ready state after login/onboarding) */
onTerminalOnboardingComplete: (callback: (info: {
terminalId: string;
profileId?: string;
detectedAt: string;
}) => void) => () => void;
/** Listen for pending Claude resume notifications (for deferred resume on tab activation) */
onTerminalPendingResume: (callback: (id: string, sessionId?: string) => void) => () => void;
/** Listen for profile change events - terminals need to be recreated with new profile env vars */
@@ -7,7 +7,7 @@ export type CredentialSource = 'oauth' | 'api-key' | 'env' | 'keychain';
export type BuiltinProvider =
| 'anthropic' | 'openai' | 'google' | 'amazon-bedrock' | 'azure'
| 'mistral' | 'groq' | 'xai' | 'openrouter' | 'zai'
| 'ollama' | 'openai-compatible';
| 'ollama' | 'openai-compatible' | 'mascarade';
export type BillingModel = 'subscription' | 'pay-per-use';
+1128 -954
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -45,6 +45,6 @@
"lucide-react": "^0.562.0"
},
"overrides": {
"@electron/rebuild": "4.0.2"
"@electron/rebuild": "4.0.3"
}
}