Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@
{"id":"agent-relay-468","title":"Implement messages API endpoints","description":"Create new router: src/cloud/api/messages.ts\n\nEndpoints:\n1. GET /api/messages/conversations\n - List all conversations for user in workspace\n - Include unread count per conversation\n - Sort by last_message_at descending\n\n2. GET /api/messages/conversations/:conversationId\n - Paginated message list (50 per page, cursor-based)\n - Include sender name/avatar lookup\n\n3. POST /api/messages/send\n - Body: { recipientId, recipientType, workspaceId, content, contentType? }\n - Create conversation if doesn't exist\n - Store message\n - Return message + conversationId\n\n4. POST /api/messages/conversations/:conversationId/read\n - Update message_read_state\n - Return success\n\n5. GET /api/messages/unread-count\n - Total unread across all conversations\n - For badge display\n\nAll endpoints require workspace access (use requireWorkspaceAccess middleware)","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:32.043028+01:00","updated_at":"2026-01-06T00:14:32.043028+01:00","dependencies":[{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-467","type":"blocks","created_at":"2026-01-06T14:07:38.45694+01:00","created_by":"import"},{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-464","type":"blocks","created_at":"2026-01-06T14:07:38.457577+01:00","created_by":"import"}]}
{"id":"agent-relay-469","title":"Extend presence WebSocket for real-time DM delivery","description":"Extend /ws/presence WebSocket handler to support DM delivery:\n\nNew message types:\n1. dm_send (client-\u003eserver)\n - recipientId, recipientType, content, tempId\n - Store message via messages API\n - Forward to recipient's WebSocket connection\n\n2. dm_received (server-\u003eclient)\n - Full message object\n - Sent to recipient when they're online\n\n3. dm_sent (server-\u003eclient)\n - Confirmation with tempId for optimistic UI\n - Sent back to sender\n\n4. dm_typing (both directions)\n - conversationId, userId, userName\n - Debounced, auto-expires after 3s\n\nAgent bridging:\n- If recipientType='agent', forward via daemon WebSocket\n- Store message for history\n- Handle agent responses coming back\n\nLocation: src/cloud/server.ts (presence WebSocket section)\n\nConsider: separate /ws/messages endpoint vs extending presence","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:44.150375+01:00","updated_at":"2026-01-06T00:14:44.150375+01:00","dependencies":[{"issue_id":"agent-relay-469","depends_on_id":"agent-relay-468","type":"blocks","created_at":"2026-01-06T14:07:38.458202+01:00","created_by":"import"}]}
{"id":"agent-relay-470","title":"Build messaging UI components for dashboard","description":"Create React components for messaging:\n\n1. ConversationList\n - Sidebar showing all DM threads\n - Unread count badges\n - Click to select conversation\n - \"New DM\" button\n\n2. MessageThread\n - Main chat view\n - Infinite scroll for history\n - New message indicator\n - Typing indicator display\n\n3. MessageInput\n - Text input with send button\n - Typing indicator emission\n - Markdown/code toggle (optional v1)\n\n4. UserSelector\n - Modal/dropdown to start new DM\n - Show online workspace members + agents\n - Search/filter\n\n5. UnreadBadge\n - Notification count component\n - Used in navigation\n\nIntegration:\n- Add to workspace dashboard layout\n- Connect to WebSocket for real-time\n- Use API for initial data + history\n\nLocation: src/dashboard/react-components/messaging/\nStyling: Follow existing Tailwind patterns","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:56.997803+01:00","updated_at":"2026-01-06T00:14:56.997803+01:00","dependencies":[{"issue_id":"agent-relay-470","depends_on_id":"agent-relay-469","type":"blocks","created_at":"2026-01-06T14:07:38.458868+01:00","created_by":"import"}]}
{"id":"agent-relay-471","title":"Use 1GB instance for free tier workspaces","description":"Reduce free tier Fly.io costs by using smaller instance. Change provisioner to use 1 CPU / 1GB RAM (shared-cpu-1x) for free tier users instead of 2 CPU / 2GB. Saves ~$8/user/month.","status":"open","priority":1,"issue_type":"task","created_at":"2026-01-06T15:47:42.391116+01:00","updated_at":"2026-01-06T15:47:42.391116+01:00"}
{"id":"agent-relay-472","title":"Add cron to enforce free tier 5-hour compute limit","description":"Add enforcement for free tier compute hours. Create a cron job that: 1) Checks all free tier users' compute usage, 2) Stops workspaces when 5 hours reached, 3) Sends notification email. Run every 15 minutes.","status":"open","priority":1,"issue_type":"task","created_at":"2026-01-06T15:47:44.48545+01:00","updated_at":"2026-01-06T15:47:44.48545+01:00"}
{"id":"agent-relay-473","title":"Add remaining compute hours banner for free tier","description":"Show free tier users their remaining compute hours in the dashboard. Display banner with: 'X of 5 hours used this month' and upgrade CTA when approaching limit (\u003e4 hours). Include in workspace header.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T15:47:45.396171+01:00","updated_at":"2026-01-06T15:47:45.396171+01:00"}
{"id":"agent-relay-47z","title":"Express 5 may have breaking changes from Express 4 patterns","description":"package.json uses [email protected] which is a major version with breaking changes from Express 4. Verify: (1) Error handling middleware patterns, (2) Router behavior, (3) Body parsing (express.json vs body-parser).","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:49.269841+01:00","updated_at":"2025-12-20T00:18:49.269841+01:00"}
{"id":"agent-relay-4e0","title":"Fix message truncation - messages cut off at source","description":"Root cause found: parser.ts:40 inline regex only captures single line. Multi-line messages are split by parsePassThrough() at line 206. Fix options: (1) Allow continuation lines in inline format, (2) Use block format for multi-line, (3) Add heuristic to join lines until next @relay pattern.","status":"closed","priority":2,"issue_type":"bug","assignee":"MistyShelter","created_at":"2025-12-19T23:40:35.082717+01:00","updated_at":"2025-12-20T00:03:54.806087+01:00","closed_at":"2025-12-20T00:03:54.806087+01:00"}
{"id":"agent-relay-4ft","title":"Merge project info into status command","status":"closed","priority":2,"issue_type":"task","assignee":"Pruner","created_at":"2025-12-19T21:59:52.685495+01:00","updated_at":"2025-12-19T22:06:44.276187+01:00","closed_at":"2025-12-19T22:06:44.276187+01:00"}
Expand Down
16 changes: 15 additions & 1 deletion src/cloud/api/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,21 @@ export const billingRouter = Router();
* Get all available billing plans
*/
billingRouter.get('/plans', (req, res) => {
const plans = getAllPlans();
const rawPlans = getAllPlans();

// Transform plans to frontend format
const plans = rawPlans.map((plan) => ({
tier: plan.id,
name: plan.name,
description: plan.description,
price: {
monthly: plan.priceMonthly / 100, // Convert cents to dollars
yearly: plan.priceYearly / 100,
},
features: plan.features,
limits: plan.limits,
recommended: plan.id === 'pro',
}));

// Add publishable key for frontend
const config = getConfig();
Expand Down
28 changes: 16 additions & 12 deletions src/cloud/api/daemons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { Router, Request, Response } from 'express';
import { randomBytes, createHash } from 'crypto';
import { requireAuth } from './auth.js';
import { db } from '../db/index.js';
import { vault } from '../vault/index.js';

export const daemonsRouter = Router();

Expand Down Expand Up @@ -235,24 +234,29 @@ daemonsRouter.post('/heartbeat', requireDaemonAuth as any, async (req: Request,

/**
* GET /api/daemons/credentials
* Get credentials for the daemon's user (syncs cloud credentials to local)
* Get credentials for the daemon's user
*
* Note: Tokens are no longer stored centrally. CLI tools authenticate directly
* on workspace/local instances. This endpoint returns connected provider info only.
*/
daemonsRouter.get('/credentials', requireDaemonAuth as any, async (req: Request, res: Response) => {
const daemon = (req as any).daemon;

try {
// Get all decrypted credentials for the user via vault
const credentialsMap = await vault.getUserCredentials(daemon.userId);

// Convert Map to array format for API response
const credentials = Array.from(credentialsMap.entries()).map(([provider, cred]) => ({
provider,
accessToken: cred.accessToken,
tokenType: 'bearer',
expiresAt: cred.tokenExpiresAt,
// Get connected providers for this user (no tokens stored centrally)
const credentials = await db.credentials.findByUserId(daemon.userId);

// Return provider info without tokens
const providers = credentials.map((cred) => ({
provider: cred.provider,
providerAccountEmail: cred.providerAccountEmail,
connectedAt: cred.createdAt,
}));

res.json({ credentials });
res.json({
providers,
note: 'Tokens are authenticated locally on workspace instances via CLI.',
});
} catch (error) {
console.error('Error fetching credentials:', error);
res.status(500).json({ error: 'Failed to fetch credentials' });
Expand Down
20 changes: 9 additions & 11 deletions src/cloud/api/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import * as crypto from 'crypto';
import { requireAuth } from './auth.js';
import { db } from '../db/index.js';
import { vault } from '../vault/index.js';

// Import for local use
import {
Expand Down Expand Up @@ -365,7 +364,7 @@
accessToken = creds.token;
refreshToken = creds.refreshToken;
if (creds.tokenExpiresAt) {
tokenExpiresAt = new Date(creds.tokenExpiresAt);

Check warning on line 367 in src/cloud/api/onboarding.ts

View workflow job for this annotation

GitHub Actions / lint

'tokenExpiresAt' is assigned a value but never used. Allowed unused vars must match /^_/u
}
console.log('[onboarding] Fetched credentials from workspace:', {
hasToken: !!accessToken,
Expand Down Expand Up @@ -414,13 +413,11 @@
});
}

// Store in vault with refresh token and expiry
await vault.storeCredential({
// Mark provider as connected (tokens are not stored centrally - CLI tools
// authenticate directly on workspace instances)
await db.credentials.upsert({
userId,
provider,
accessToken,
refreshToken,
tokenExpiresAt,
scopes: getProviderScopes(provider),
});

Expand Down Expand Up @@ -568,22 +565,23 @@
return res.status(400).json({ error: 'Invalid token' });
}

// Store in vault
await vault.storeCredential({
// Mark provider as connected (tokens are not stored centrally - CLI tools
// authenticate directly on workspace instances)
await db.credentials.upsert({
userId,
provider,
accessToken: token,
scopes: getProviderScopes(provider),
providerAccountEmail: email,
});

res.json({
success: true,
message: `${provider} connected successfully`,
note: 'Token validated. Configure this on your workspace for usage.',
});
} catch (error) {
console.error(`Error storing token for ${provider}:`, error);
res.status(500).json({ error: 'Failed to store token' });
console.error(`Error storing provider connection for ${provider}:`, error);
res.status(500).json({ error: 'Failed to store provider connection' });
}
});

Expand Down
28 changes: 12 additions & 16 deletions src/cloud/api/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { createClient } from 'redis';
import { requireAuth } from './auth.js';
import { getConfig } from '../config.js';
import { db } from '../db/index.js';
import { vault } from '../vault/index.js';

export const providersRouter = Router();

Expand Down Expand Up @@ -334,20 +333,19 @@ providersRouter.post('/:provider/verify', async (req: Request, res: Response) =>
// In production, we'd verify by making a test API call with the credentials

try {
// For now, mark as connected (in production, verify credentials exist)
// This would be called after the user's workspace detects valid credentials
// Mark as connected (tokens are not stored centrally - CLI tools
// authenticate directly on workspace instances)
await db.credentials.upsert({
userId,
provider,
accessToken: 'cli-authenticated', // Placeholder - real token from CLI
scopes: [], // CLI auth doesn't use scopes
providerAccountEmail: req.body.email, // User can optionally provide
});

res.json({
success: true,
message: `${providerConfig.displayName} connected via CLI`,
note: 'Credentials will be synced when workspace starts',
note: 'CLI credentials remain on your local machine',
});
} catch (error) {
console.error(`Error verifying ${provider} auth:`, error);
Expand Down Expand Up @@ -403,18 +401,19 @@ providersRouter.post('/:provider/api-key', async (req: Request, res: Response) =
return res.status(400).json({ error: 'Invalid API key' });
}

// Store the API key - use scopes from device flow providers, empty for CLI providers
// Mark provider as connected (tokens are not stored centrally - CLI tools
// authenticate directly on workspace instances)
const scopes = isDeviceFlowProvider(providerConfig) ? providerConfig.scopes : [];
await vault.storeCredential({
await db.credentials.upsert({
userId,
provider,
accessToken: apiKey,
scopes,
});

res.json({
success: true,
message: `${providerConfig.displayName} connected`,
note: 'API key validated. Configure this key on your workspace for usage.',
});
} catch (error) {
console.error(`Error connecting ${provider} with API key:`, error);
Expand Down Expand Up @@ -592,7 +591,9 @@ async function pollForToken(flowId: string, provider: ProviderType, clientId: st
}

/**
* Store tokens after successful device flow
* Mark provider as connected after successful device flow
* Note: Tokens are not stored centrally - CLI tools authenticate directly
* on workspace instances. We only record the connection status and user info.
*/
async function storeProviderTokens(
userId: string,
Expand Down Expand Up @@ -623,15 +624,10 @@ async function storeProviderTokens(
}
}

// Encrypt and store
await vault.storeCredential({
// Mark provider as connected (without storing tokens)
await db.credentials.upsert({
userId,
provider,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
tokenExpiresAt: tokens.expiresIn
? new Date(Date.now() + tokens.expiresIn * 1000)
: undefined,
scopes: tokens.scope?.split(' '),
providerAccountId: userInfo.id,
providerAccountEmail: userInfo.email,
Expand Down
Loading