Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8bfe675
fix(copilot): consolidate completion billing and graph-only workflow …
BruzWJ Jun 11, 2026
8fabe73
fix(workflows): unify trigger resolution for editor and queued runs (…
BruzWJ Jun 15, 2026
fbdfad0
feat(nav): support authenticated public navigation (#143)
BruzWJ Jun 16, 2026
9b081eb
fix: settle Stripe subscription deletes and render sidebar profile tr…
BruzWJ Jun 17, 2026
bb59698
fix(auth): use browser origin for client auth requests (#145)
BruzWJ Jun 17, 2026
a4976b5
fix(tradinggoose): harden proxy auth recovery and runtime base URLs (…
BruzWJ Jun 18, 2026
ec0b5f4
fix(auth): use deployment-aware Better Auth cookies (#147)
BruzWJ Jun 18, 2026
6a2dd85
fix(auth): remove forced uncached session reads (#148)
BruzWJ Jun 18, 2026
75435f1
fix(proxy): use Better Auth session cookie detection (#149)
BruzWJ Jun 18, 2026
02c2a81
fix(auth): unify callback-safe session recovery (#150)
BruzWJ Jun 18, 2026
3692ee1
fix(workspaces): centralize access checks and auth redirects (#151)
BruzWJ Jun 19, 2026
61514c4
refactor(workspace-permissions): thread authenticated user id through…
BruzWJ Jun 19, 2026
3c0cca3
fix(socket): log socket auth failures without redirecting (#153)
BruzWJ Jun 19, 2026
61dc75e
feat(copilot): localize mention labels and mention search (#155)
agualdron Jun 23, 2026
550ab03
feat(copilot-mcp): expose TradingGoose tools to trusted agents (#157)
BruzWJ Jun 29, 2026
7f0dfee
fix(yjs): move entity lists onto shared realtime sessions (#160)
BruzWJ Jul 4, 2026
983c73e
feat(i18n): add catalog scanner for localization coverage
agualdron Jul 6, 2026
9455388
style(i18n): align catalog tooling with biome
agualdron Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bunx lint-staged
bunx lint-staged
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
- Do not add legacy support; updates should be clean and avoid extra project complexity.
- We do not need any form of legacy support as the project is under fresh dev, do not add any form of legacy backfill path
- This project does not support any legacy methods.
- Ignore all license related issues.
- Project uses `Bun` pacakge manager with turborepo.
- Project uses `Bun` pacakge manager with turborepo, find project defined scripts in `/pacakge.json` for testing.
- Prefer removing lines of code over adding more lines of code to reduce project complexity.

## Planning
Expand Down
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,33 @@ It is built for analytics, research, charting, monitoring, and workflow automati
<img alt="Project Overview" src="apps/tradinggoose/public/static/preview-light.png" width="2559">
</picture>

---

### Copilot-MCP

You can install TradingGoose MCP to use any local agentic tool like Codex, Claude Code, Cursor, ZCode as Copilot to perform TradingGoose-Studio operations

#### Mac/Linux:
connect to the hosted instance:
```
curl -fsSL https://TradingGoose.ai/mcp/setup | sh
```

connect to self-hosted instance:
```
curl -fsSL http://localhost:3000/mcp/setup | sh
```

#### Windows
connect to the hosted instance:
```
irm https://TradingGoose.ai/mcp/setup | iex
```

connect to self-hosted instance:
```
irm http://localhost:3000/mcp/setup | iex
```

## Quick Start

Expand Down Expand Up @@ -86,12 +112,10 @@ cd ../../packages/db && cp .env.example .env

#### 4. Run database migrations
```
cd packages/db
bunx drizzle-kit migrate --config=./drizzle.config.ts
bun run db:migrate
```
#### 5. Start development servers
#### 5. Start full development servers
```
cd ../..
bun run dev:full
```

Expand All @@ -101,9 +125,9 @@ If you use Docker Compose, copy `apps/tradinggoose/.env.example.docker` to
`apps/tradinggoose/.env` and set the required secrets before running the
compose manifests. The `.env` must include `POSTGRES_*`,
`NEXT_PUBLIC_APP_URL`, `NEXT_PUBLIC_SOCKET_URL`, `BETTER_AUTH_SECRET`,
`ENCRYPTION_KEY`, `API_ENCRYPTION_KEY`, and `INTERNAL_API_SECRET`. The
`ENCRYPTION_KEY` value is shared by both the app and realtime containers, and
`API_ENCRYPTION_KEY` enables encrypted API-key storage in the app container.
`ENCRYPTION_KEY`, and `INTERNAL_API_SECRET`. Set `API_ENCRYPTION_KEY` when
API-key access or MCP token issuance is used; it encrypts API keys at rest in
the app container.
`NEXT_PUBLIC_SOCKET_URL` should point at `http://localhost:3002` for local
Compose runs; production deployments must override it with a browser-reachable
public URL. The prod and Ollama compose files also require `IMAGE_TAG` and
Expand Down
18 changes: 3 additions & 15 deletions apps/tradinggoose/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
# - Configure system-managed integration OAuth apps in Admin Integrations instead:
# google-drive, google-email, github-repo, microsoft, slack, reddit, etc.
# - Platform-managed vars like NODE_ENV, NEXT_RUNTIME, and VERCEL are omitted on purpose.
# - Internal React Email preview vars like EMAILS_DIR_ABSOLUTE_PATH and
# PREVIEW_SERVER_LOCATION are also omitted on purpose.
# - Internal React Email preview vars are omitted on purpose.
#
# Notes:
# - Boolean-like flags should use true/false unless noted otherwise.
Expand Down Expand Up @@ -44,9 +43,9 @@ BETTER_AUTH_SECRET="replace-with-64-hex-characters"
# Generate a secure 64-character hex secret.
ENCRYPTION_KEY="replace-with-64-hex-characters"

# Recommended: dedicated encryption key for stored API credentials.
# Optional unless API-key access or MCP token issuance is used.
# Generate a secure 64-character hex secret.
API_ENCRYPTION_KEY="replace-with-64-hex-characters"
API_ENCRYPTION_KEY=""

# Required: internal server-to-server auth secret used by app routes, sockets,
# cron endpoints, and other internal calls.
Expand Down Expand Up @@ -232,14 +231,6 @@ KB_CONFIG_DELAY_BETWEEN_DOCUMENTS="50"
# NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_DISABLED="1"

###############################################################################
# Email preview / local tooling
###############################################################################

# Optional: only used by React Email preview and similar local tooling when
# NEXT_PUBLIC_APP_URL is not available in that process.
# EMAILS_PREVIEW_BASE_URL="http://localhost:3000"

###############################################################################
# Deployment and local infrastructure helpers
###############################################################################
Expand All @@ -255,6 +246,3 @@ NEXT_PUBLIC_POSTHOG_DISABLED="1"

# Optional: tell the app it is running inside a Docker image build.
DOCKER_BUILD="false"

# Optional: landing-page preview/dev helper used by getBaseUrl().
# NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT="false"
3 changes: 3 additions & 0 deletions apps/tradinggoose/.env.example.docker
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ OLLAMA_IMAGE_TAG=latest
# Security (Required)
# Use `openssl rand -hex 32` to generate, used to encrypt environment variables
ENCRYPTION_KEY=generate-the-key
# Optional unless API-key access or MCP token issuance is used.
# Use `openssl rand -hex 32` to generate, used to encrypt API keys
API_ENCRYPTION_KEY=
# Use `openssl rand -hex 32` to generate, used to encrypt internal api routes
INTERNAL_API_SECRET=generate-the-secret

Expand Down
110 changes: 95 additions & 15 deletions apps/tradinggoose/app/(auth)/auth-locale-redirects.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { VerifyContent } from './verify/verify-content'
const mockPush = vi.hoisted(() => vi.fn())
const mockSignUpEmail = vi.hoisted(() => vi.fn())
const mockSignInEmail = vi.hoisted(() => vi.fn())
const mockSignOut = vi.hoisted(() => vi.fn())
const mockSendVerificationOtp = vi.hoisted(() => vi.fn())
const mockRefetchSession = vi.hoisted(() => vi.fn())
const mockUseVerification = vi.hoisted(() => vi.fn())
Expand Down Expand Up @@ -45,6 +46,7 @@ vi.mock('@/i18n/navigation', () => ({
useRouter: () => ({
push: mockPush,
}),
usePathname: () => '/login',
}))

vi.mock('@/lib/auth-client', () => ({
Expand All @@ -55,6 +57,7 @@ vi.mock('@/lib/auth-client', () => ({
signIn: {
email: mockSignInEmail,
},
signOut: mockSignOut,
emailOtp: {
sendVerificationOtp: mockSendVerificationOtp,
},
Expand Down Expand Up @@ -107,7 +110,11 @@ vi.mock('@/components/ui/label', () => ({
...props
}: React.LabelHTMLAttributes<HTMLLabelElement> & {
children?: React.ReactNode
}) => <label {...props}>{children}</label>,
}) => (
<label {...props} htmlFor={props.htmlFor ?? 'test-field'}>
{children}
</label>
),
}))

vi.mock('@/components/ui/dialog', () => ({
Expand Down Expand Up @@ -149,6 +156,7 @@ describe('auth locale redirects', () => {
mockPush.mockReset()
mockSignUpEmail.mockReset()
mockSignInEmail.mockReset()
mockSignOut.mockReset()
mockSendVerificationOtp.mockReset()
mockRefetchSession.mockReset()
mockUseVerification.mockReset()
Expand All @@ -162,6 +170,7 @@ describe('auth locale redirects', () => {
root.unmount()
})
container.remove()
vi.useRealTimers()
vi.restoreAllMocks()
reactActEnvironment.IS_REACT_ACT_ENVIRONMENT = false
})
Expand Down Expand Up @@ -204,6 +213,18 @@ describe('auth locale redirects', () => {
})
}

async function renderLogin(locale: 'en' | 'es' | 'zh' = 'en') {
await renderWithLocale(
locale,
<LoginPage
githubAvailable={false}
googleAvailable={false}
isProduction={false}
registrationMode='open'
/>
)
}

it.each(['es', 'zh'] as const)(
'pushes the canonical verify path after signup for %s',
async (locale) => {
Expand All @@ -226,6 +247,8 @@ describe('auth locale redirects', () => {
await setInputValue('#password', 'Password1!')
await submitRenderedForm()

expect(mockRefetchSession).not.toHaveBeenCalled()
expect(mockFetch).not.toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/verify?fromSignup=true')
}
)
Expand All @@ -235,15 +258,7 @@ describe('auth locale redirects', () => {
async (locale) => {
mockSignInEmail.mockRejectedValue({ code: 'EMAIL_NOT_VERIFIED' })

await renderWithLocale(
locale,
<LoginPage
githubAvailable={false}
googleAvailable={false}
isProduction={false}
registrationMode='open'
/>
)
await renderLogin(locale)

await setInputValue('#email', 'ada@example.com')
await setInputValue('#password', 'Password1!')
Expand All @@ -253,6 +268,75 @@ describe('auth locale redirects', () => {
}
)

it('runs reauth cleanup on arrival and waits before direct login starts', async () => {
vi.useFakeTimers()
testState.searchParams = new URLSearchParams('reauth=1&callbackUrl=%2Fworkspace')
const cleanupSignalRef: { current: AbortSignal | null } = { current: null }
mockSignOut.mockImplementation((options) => {
cleanupSignalRef.current = options?.fetchOptions?.signal ?? null
return new Promise(() => {})
})
mockSignInEmail.mockResolvedValue({})

await renderLogin()

expect(mockSignOut).toHaveBeenCalledTimes(1)
expect(container.querySelector('form')).toBeInstanceOf(HTMLFormElement)

await setInputValue('#email', 'ada@example.com')
await setInputValue('#password', 'Password1!')

await submitRenderedForm()

expect(mockSignInEmail).not.toHaveBeenCalled()

await act(async () => {
await vi.runOnlyPendingTimersAsync()
await Promise.resolve()
})

expect(cleanupSignalRef.current?.aborted).toBe(true)
expect(mockSignInEmail).toHaveBeenCalledTimes(1)
})

it.each([
'FAILED_TO_CREATE_SESSION',
'UNABLE_TO_CREATE_SESSION',
'FAILED_TO_GET_SESSION',
'SESSION_EXPIRED',
])('runs reauth cleanup when direct login returns %s', async (errorCode) => {
mockSignInEmail.mockResolvedValue({ error: { code: errorCode } })
mockSignOut.mockReturnValue(new Promise(() => {}))

await renderLogin()

await setInputValue('#email', 'ada@example.com')
await setInputValue('#password', 'Password1!')
await submitRenderedForm()

expect(mockSignInEmail).toHaveBeenCalledTimes(1)
expect(mockSignOut).toHaveBeenCalledTimes(1)
expect(container.textContent).toContain(getPublicCopy('en').auth.login.errors.unableToSignInNow)
})

it('keeps invalid credential failures on the login form', async () => {
mockSignInEmail.mockResolvedValue({
error: { code: 'INVALID_CREDENTIALS', status: 401 },
})

await renderLogin()

await setInputValue('#email', 'ada@example.com')
await setInputValue('#password', 'wrong-password')
await submitRenderedForm()

expect(mockSignOut).not.toHaveBeenCalled()
expect(container.querySelector('form')).toBeInstanceOf(HTMLFormElement)
expect(container.textContent).toContain(
getPublicCopy('en').auth.login.errors.invalidCredentials
)
})

it('pushes the canonical signup path from the verify screen back action', async () => {
mockUseVerification.mockReturnValue({
otp: '',
Expand All @@ -272,11 +356,7 @@ describe('auth locale redirects', () => {

await renderWithLocale(
'en',
<VerifyContent
hasEmailService
isProduction={false}
isEmailVerificationEnabled
/>
<VerifyContent hasEmailService isProduction={false} isEmailVerificationEnabled />
)

const backButton = Array.from(container.querySelectorAll('button')).find(
Expand Down
Loading
Loading