Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ import { createCodexRoutes } from './routes/codex/index.js';
import { CodexUsageService } from './services/codex-usage-service.js';
import { CodexAppServerService } from './services/codex-app-server-service.js';
import { CodexModelCacheService } from './services/codex-model-cache-service.js';
import { createZaiRoutes } from './routes/zai/index.js';
import { ZaiUsageService } from './services/zai-usage-service.js';
import { createGeminiRoutes } from './routes/gemini/index.js';
import { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
Expand Down Expand Up @@ -326,6 +329,7 @@ const claudeUsageService = new ClaudeUsageService();
const codexAppServerService = new CodexAppServerService();
const codexModelCacheService = new CodexModelCacheService(DATA_DIR, codexAppServerService);
const codexUsageService = new CodexUsageService(codexAppServerService);
const zaiUsageService = new ZaiUsageService();
const mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader);

Expand Down Expand Up @@ -434,6 +438,8 @@ app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/codex', createCodexRoutes(codexUsageService, codexModelCacheService));
app.use('/api/zai', createZaiRoutes(zaiUsageService, settingsService));
app.use('/api/gemini', createGeminiRoutes());
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));
Expand Down
60 changes: 60 additions & 0 deletions apps/server/src/routes/gemini/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Router, Request, Response } from 'express';
import { GeminiProvider } from '../../providers/gemini-provider.js';
import { getGeminiUsageService } from '../../services/gemini-usage-service.js';
import { createLogger } from '@automaker/utils';

const logger = createLogger('Gemini');

export function createGeminiRoutes(): Router {
const router = Router();

// Get current usage/quota data from Google Cloud API
router.get('/usage', async (_req: Request, res: Response) => {
try {
const usageService = getGeminiUsageService();
const usageData = await usageService.fetchUsageData();

res.json(usageData);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error('Error fetching Gemini usage:', error);

// Return error in a format the UI expects
res.status(200).json({
authenticated: false,
authMethod: 'none',
usedPercent: 0,
remainingPercent: 100,
lastUpdated: new Date().toISOString(),
error: `Failed to fetch Gemini usage: ${message}`,
});
Comment on lines +22 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The catch block for the /usage route currently returns a 200 status with authenticated: false for any unexpected error. This can be misleading, as an error (e.g., a file parsing error for credentials) doesn't necessarily mean the user is unauthenticated. For unexpected server-side errors, it's more appropriate to return a 500 status code. This provides a clearer signal to the client that something went wrong on the server, distinct from a normal "not authenticated" state which is handled gracefully within the service.

      res.status(500).json({
        error: `An unexpected error occurred while fetching Gemini usage: ${message}`,
      });

}
});

// Check if Gemini is available
router.get('/status', async (_req: Request, res: Response) => {
try {
const provider = new GeminiProvider();
const status = await provider.detectInstallation();

const authMethod =
(status as any).authMethod ||
(status.authenticated ? (status.hasApiKey ? 'api_key' : 'cli_login') : 'none');

res.json({
success: true,
installed: status.installed,
version: status.version || null,
path: status.path || null,
authenticated: status.authenticated || false,
authMethod,
hasCredentialsFile: (status as any).hasCredentialsFile || false,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ success: false, error: message });
}
});
Comment on lines +35 to +57
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find GeminiProvider class and detectInstallation method
find . -type f -name "*.ts" -o -name "*.js" | xargs grep -l "class GeminiProvider" | head -5

Repository: AutoMaker-Org/automaker

Length of output: 113


🏁 Script executed:

#!/bin/bash
# Search for detectInstallation method definition
rg "detectInstallation" --type ts --type tsx -B 5 -A 15

Repository: AutoMaker-Org/automaker

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Search for detectInstallation method with corrected flags
rg "detectInstallation" --type ts -B 5 -A 20

Repository: AutoMaker-Org/automaker

Length of output: 50379


🏁 Script executed:

#!/bin/bash
# Read the GeminiProvider file
wc -l ./apps/server/src/providers/gemini-provider.ts

Repository: AutoMaker-Org/automaker

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Read the detectInstallation method in gemini-provider.ts
sed -n '110,145p' ./apps/server/src/providers/gemini-provider.ts

Repository: AutoMaker-Org/automaker

Length of output: 1064


🏁 Script executed:

#!/bin/bash
# Also check InstallationStatus type definition
rg "interface InstallationStatus|type InstallationStatus" --type ts -A 15

Repository: AutoMaker-Org/automaker

Length of output: 963


🏁 Script executed:

#!/bin/bash
# Read the full detectInstallation method in gemini-provider.ts to see exact return
rg "async detectInstallation" ./apps/server/src/providers/gemini-provider.ts -A 25

Repository: AutoMaker-Org/automaker

Length of output: 731


🏁 Script executed:

#!/bin/bash
# Check if there's any place where status is modified before being used in the route
head -60 ./apps/server/src/routes/gemini/index.ts

Repository: AutoMaker-Org/automaker

Length of output: 2061


Remove unnecessary as any casts; the accessed fields don't exist on InstallationStatus.

Lines 40-42 and 51 use (status as any) to access authMethod and hasCredentialsFile, which are not defined in the InstallationStatus interface. The code already has proper fallback logic that computes authMethod from existing fields; simply remove the unsafe cast and rely on the fallback computation. For hasCredentialsFile, either add it to the InstallationStatus type if needed, or remove it from the response and default to false.

🤖 Prompt for AI Agents
In `@apps/server/src/routes/gemini/index.ts` around lines 35 - 57, Remove the
unsafe (status as any) casts: use the typed InstallationStatus returned by
GeminiProvider.detectInstallation() and compute authMethod using the existing
fields (status.authenticated / status.hasApiKey) without casting; also remove or
type-augment hasCredentialsFile—either add hasCredentialsFile to the
InstallationStatus interface (and populate it in detectInstallation) or stop
returning hasCredentialsFile and default it to false in the JSON response;
update the route in apps/server/src/routes/gemini/index.ts (references:
GeminiProvider, detectInstallation, authMethod, hasCredentialsFile,
InstallationStatus) accordingly.


return router;
}
179 changes: 179 additions & 0 deletions apps/server/src/routes/zai/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { Router, Request, Response } from 'express';
import { ZaiUsageService } from '../../services/zai-usage-service.js';
import type { SettingsService } from '../../services/settings-service.js';
import { createLogger } from '@automaker/utils';

const logger = createLogger('Zai');

export function createZaiRoutes(
usageService: ZaiUsageService,
settingsService: SettingsService
): Router {
const router = Router();

// Initialize z.ai API token from credentials on startup
(async () => {
try {
const credentials = await settingsService.getCredentials();
if (credentials.apiKeys?.zai) {
usageService.setApiToken(credentials.apiKeys.zai);
logger.info('[init] Loaded z.ai API key from credentials');
}
} catch (error) {
logger.error('[init] Failed to load z.ai API key from credentials:', error);
}
})();

// Get current usage (fetches from z.ai API)
router.get('/usage', async (_req: Request, res: Response) => {
try {
// Check if z.ai API is configured
const isAvailable = usageService.isAvailable();
if (!isAvailable) {
// Use a 200 + error payload so the UI doesn't interpret it as session auth error
res.status(200).json({
error: 'z.ai API not configured',
message: 'Set Z_AI_API_KEY environment variable to enable z.ai usage tracking',
});
return;
}

const usage = await usageService.fetchUsageData();
res.json(usage);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';

if (message.includes('not configured') || message.includes('API token')) {
res.status(200).json({
error: 'API token required',
message: 'Set Z_AI_API_KEY environment variable to enable z.ai usage tracking',
});
} else if (message.includes('failed') || message.includes('request')) {
res.status(200).json({
error: 'API request failed',
message: message,
});
} else {
logger.error('Error fetching z.ai usage:', error);
res.status(500).json({ error: message });
}
}
});

// Configure API token (for settings page)
router.post('/configure', async (req: Request, res: Response) => {
try {
const { apiToken, apiHost } = req.body;

if (apiToken !== undefined) {
// Set in-memory token
usageService.setApiToken(apiToken || '');

// Persist to credentials (deep merge happens in updateCredentials)
try {
await settingsService.updateCredentials({
apiKeys: { zai: apiToken || '' },
} as Parameters<typeof settingsService.updateCredentials>[0]);
logger.info('[configure] Saved z.ai API key to credentials');
} catch (persistError) {
logger.error('[configure] Failed to persist z.ai API key:', persistError);
}
}

if (apiHost) {
usageService.setApiHost(apiHost);
}

res.json({
success: true,
message: 'z.ai configuration updated',
isAvailable: usageService.isAvailable(),
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error('Error configuring z.ai:', error);
res.status(500).json({ error: message });
}
});

// Verify API key without storing it (for testing in settings)
router.post('/verify', async (req: Request, res: Response) => {
try {
const { apiKey } = req.body;

if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) {
res.json({
success: false,
authenticated: false,
error: 'Please provide an API key to test.',
});
return;
}

// Test the key by making a request to z.ai API
const quotaUrl =
process.env.Z_AI_QUOTA_URL ||
`${process.env.Z_AI_API_HOST ? `https://${process.env.Z_AI_API_HOST}` : 'https://api.z.ai'}/api/monitor/usage/quota/limit`;

logger.info(`[verify] Testing API key against: ${quotaUrl}`);

const response = await fetch(quotaUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${apiKey.trim()}`,
Accept: 'application/json',
},
});
Comment on lines +113 to +126
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The logic for constructing the quotaUrl and performing the API key verification is implemented directly in this route handler. This duplicates logic from ZaiUsageService and couples the route tightly with the external API's implementation details. To improve separation of concerns and code reuse, this verification logic should be moved into a new method on the ZaiUsageService.


if (response.ok) {
res.json({
success: true,
authenticated: true,
message: 'Connection successful! z.ai API responded.',
});
} else if (response.status === 401 || response.status === 403) {
res.json({
success: false,
authenticated: false,
error: 'Invalid API key. Please check your key and try again.',
});
} else {
res.json({
success: false,
authenticated: false,
error: `API request failed: ${response.status} ${response.statusText}`,
});
}
} catch (error) {
Comment on lines +120 to +147
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find package.json files in the project root and apps/server to check Node engine requirements
fd package.json -t f -E node_modules -E dist -E build | head -5

Repository: AutoMaker-Org/automaker

Length of output: 211


🏁 Script executed:

#!/bin/bash
# Get the actual content of the file in question
cat -n apps/server/src/routes/zai/index.ts | head -170 | tail -60

Repository: AutoMaker-Org/automaker

Length of output: 2408


🏁 Script executed:

#!/bin/bash
# Check for service directory and existing service files in apps/server
find apps/server/src/services -type f -name "*zai*" 2>/dev/null || echo "No zai service found"
ls -la apps/server/src/services/ 2>/dev/null | head -20

Repository: AutoMaker-Org/automaker

Length of output: 1493


🏁 Script executed:

#!/bin/bash
# Check Node engine requirement in apps/server package.json
cat apps/server/package.json | grep -A 5 '"engines"'

Repository: AutoMaker-Org/automaker

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Get fuller context of the zai route file to check for event emission
cat -n apps/server/src/routes/zai/index.ts | head -160

Repository: AutoMaker-Org/automaker

Length of output: 6514


🏁 Script executed:

#!/bin/bash
# Check zai-usage-service to understand service structure
head -60 apps/server/src/services/zai-usage-service.ts

Repository: AutoMaker-Org/automaker

Length of output: 1496


Add a timeout to the external z.ai verification request.

Without a timeout, the fetch request can hang indefinitely during upstream stalls, blocking the server thread. Your Node 22 runtime fully supports AbortSignal.timeout(), so add it directly:

Suggested fix
      const response = await fetch(quotaUrl, {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${apiKey.trim()}`,
          Accept: 'application/json',
        },
+        signal: AbortSignal.timeout(5000),
      });
🤖 Prompt for AI Agents
In `@apps/server/src/routes/zai/index.ts` around lines 120 - 147, The z.ai
verification fetch call can hang indefinitely; update the fetch invocation in
the route handler that calls quotaUrl to pass a signal created via
AbortSignal.timeout(timeoutMs) (e.g., const controllerSignal =
AbortSignal.timeout(5000)) in the fetch options so the request auto-aborts after
a reasonable timeout, and enhance the catch block to handle aborts (detect
AbortError / error.name === 'AbortError' and return a clear timeout JSON
response). Ensure you reference the existing fetch call that sets headers
Authorization/Accept and the surrounding try/catch so you only add the signal
option and the abort-specific error branch.

const message = error instanceof Error ? error.message : 'Unknown error';
logger.error('Error verifying z.ai API key:', error);
res.json({
success: false,
authenticated: false,
error: `Network error: ${message}`,
});
}
Comment on lines +63 to +155
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move z.ai config/verify logic into a service and emit events.
The route performs provider configuration and verification directly and doesn’t emit events for these operations. Please delegate this work to ZaiUsageService (or a dedicated service) and emit corresponding events via the shared emitter so the UI can react in real time. As per coding guidelines: 'Server business logic should be organized into services in the services/ directory, with Express route handlers in routes/ that delegate to services' and 'All server operations should emit events using createEventEmitter() from lib/events.ts that stream to the frontend via WebSocket'.

🤖 Prompt for AI Agents
In `@apps/server/src/routes/zai/index.ts` around lines 63 - 155, The route
handlers for router.post('/configure') and router.post('/verify') contain
business logic (usageService.setApiToken/setApiHost,
settingsService.updateCredentials, fetch to quota URL and response handling) and
must be moved into ZaiUsageService (or a new service) as methods like
configure({apiToken, apiHost}) and verifyApiKey(apiKey); those service methods
should perform the persistence (call settingsService.updateCredentials), runtime
updates (usageService calls), external fetch, and emit events via the shared
emitter from createEventEmitter() (e.g., emit 'zai.configured' and
'zai.verify.result' with status and error info). Update the route handlers to
simply call ZaiUsageService.configure(...) and
ZaiUsageService.verifyApiKey(...), await results, and return minimal JSON
responses, handling exceptions only to map errors to HTTP responses. Ensure you
reference and replace direct calls to usageService and
settingsService.updateCredentials inside the service and add emitter.emit calls
for UI updates.

});

// Check if z.ai is available
router.get('/status', async (_req: Request, res: Response) => {
try {
const isAvailable = usageService.isAvailable();
const hasEnvApiKey = Boolean(process.env.Z_AI_API_KEY);
const hasApiKey = usageService.getApiToken() !== null;

res.json({
success: true,
available: isAvailable,
hasApiKey,
hasEnvApiKey,
message: isAvailable ? 'z.ai API is configured' : 'z.ai API token not configured',
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ success: false, error: message });
}
});

return router;
}
Loading