claudedesk 3.7.1 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/api/terminal-routes.d.ts.map +1 -1
  2. package/dist/api/terminal-routes.js +316 -5
  3. package/dist/api/terminal-routes.js.map +1 -1
  4. package/dist/api/workspace-routes.d.ts.map +1 -1
  5. package/dist/api/workspace-routes.js +222 -0
  6. package/dist/api/workspace-routes.js.map +1 -1
  7. package/dist/client/assets/index-Ca-cAQep.js +7846 -0
  8. package/dist/client/assets/index-WKt2pA41.css +1 -0
  9. package/dist/client/index.html +2 -2
  10. package/dist/config/workspaces.d.ts +77 -0
  11. package/dist/config/workspaces.d.ts.map +1 -1
  12. package/dist/config/workspaces.js +97 -0
  13. package/dist/config/workspaces.js.map +1 -1
  14. package/dist/core/allocator-manager.d.ts +98 -0
  15. package/dist/core/allocator-manager.d.ts.map +1 -0
  16. package/dist/core/allocator-manager.js +401 -0
  17. package/dist/core/allocator-manager.js.map +1 -0
  18. package/dist/core/github-integration.d.ts +30 -1
  19. package/dist/core/github-integration.d.ts.map +1 -1
  20. package/dist/core/github-integration.js +153 -10
  21. package/dist/core/github-integration.js.map +1 -1
  22. package/dist/core/github-oauth.d.ts.map +1 -1
  23. package/dist/core/github-oauth.js +1 -0
  24. package/dist/core/github-oauth.js.map +1 -1
  25. package/dist/core/idea-manager.d.ts.map +1 -1
  26. package/dist/core/idea-manager.js +6 -2
  27. package/dist/core/idea-manager.js.map +1 -1
  28. package/dist/core/terminal-session.d.ts +1 -0
  29. package/dist/core/terminal-session.d.ts.map +1 -1
  30. package/dist/core/terminal-session.js +48 -2
  31. package/dist/core/terminal-session.js.map +1 -1
  32. package/dist/core/token-encryption.d.ts +35 -0
  33. package/dist/core/token-encryption.d.ts.map +1 -0
  34. package/dist/core/token-encryption.js +162 -0
  35. package/dist/core/token-encryption.js.map +1 -0
  36. package/package.json +1 -1
  37. package/dist/client/assets/index-C1EMDno6.js +0 -7846
  38. package/dist/client/assets/index-DyFFEzMu.css +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"terminal-routes.d.ts","sourceRoot":"","sources":["../../src/api/terminal-routes.ts"],"names":[],"mappings":"AAmRA,eAAO,MAAM,cAAc,4CAAW,CAAC"}
1
+ {"version":3,"file":"terminal-routes.d.ts","sourceRoot":"","sources":["../../src/api/terminal-routes.ts"],"names":[],"mappings":"AAoRA,eAAO,MAAM,cAAc,4CAAW,CAAC"}
@@ -15,6 +15,7 @@ import { gitlabIntegration } from '../core/gitlab-integration.js';
15
15
  import { usageManager } from '../core/usage-manager.js';
16
16
  import { settingsManager } from '../config/settings.js';
17
17
  import { queryClaudeQuota, clearQuotaCache } from '../core/claude-usage-query.js';
18
+ import { allocatorManager } from '../core/allocator-manager.js';
18
19
  import { contextManager } from '../core/context-manager.js';
19
20
  /**
20
21
  * Try to refresh a GitLab token using the refresh token.
@@ -356,6 +357,16 @@ terminalRouter.post('/sessions', (req, res) => {
356
357
  return;
357
358
  }
358
359
  }
360
+ // Budget: block new sessions if degradation is active
361
+ const budgetResult = allocatorManager.checkBudgetLimits();
362
+ if (budgetResult.activeDegradations.some((d) => d.type === 'block-new-sessions')) {
363
+ res.status(429).json({
364
+ success: false,
365
+ error: 'New sessions blocked due to high API usage. Wait for quota to reset or adjust budget settings.',
366
+ budgetBlocked: true,
367
+ });
368
+ return;
369
+ }
359
370
  const session = terminalSessionManager.createSession(ids, worktreeOptions);
360
371
  // Set handoff summary if provided (e.g., from idea promotion)
361
372
  if (handoffSummary && typeof handoffSummary === 'string') {
@@ -903,6 +914,24 @@ terminalRouter.patch('/sessions/:id/mode', (req, res) => {
903
914
  res.status(400).json({ success: false, error: errorMsg });
904
915
  }
905
916
  });
917
+ // Set session model override (budget allocator / manual)
918
+ terminalRouter.patch('/sessions/:id/model', (req, res) => {
919
+ try {
920
+ const { id } = req.params;
921
+ const { model } = req.body; // string or null to clear
922
+ const session = terminalSessionManager.getSession(id);
923
+ if (!session) {
924
+ res.status(404).json({ success: false, error: 'Session not found' });
925
+ return;
926
+ }
927
+ session.modelOverride = model || undefined;
928
+ res.json({ success: true, data: { model: session.modelOverride } });
929
+ }
930
+ catch (error) {
931
+ const errorMsg = error instanceof Error ? error.message : String(error);
932
+ res.status(400).json({ success: false, error: errorMsg });
933
+ }
934
+ });
906
935
  // Export session as markdown or JSON
907
936
  terminalRouter.get('/sessions/:id/export', (req, res) => {
908
937
  try {
@@ -2463,9 +2492,21 @@ terminalRouter.post('/sessions/:id/generate-pr-content', async (req, res) => {
2463
2492
  commitMessages = '';
2464
2493
  }
2465
2494
  }
2466
- // Get file changes summary (diff stat)
2495
+ // Get file changes summary (diff stat) - include both committed and uncommitted changes
2467
2496
  let fileChanges = '';
2468
2497
  let diffSample = '';
2498
+ // First check for uncommitted changes (staged + unstaged)
2499
+ let uncommittedChanges = '';
2500
+ try {
2501
+ uncommittedChanges = execSync('git diff HEAD --stat', {
2502
+ cwd: workingDir,
2503
+ encoding: 'utf-8',
2504
+ timeout: 10000,
2505
+ }).trim();
2506
+ }
2507
+ catch {
2508
+ // Ignore errors
2509
+ }
2469
2510
  try {
2470
2511
  const mergeBase = execSync(`git merge-base ${diffRef} HEAD`, {
2471
2512
  cwd: workingDir,
@@ -2485,9 +2526,20 @@ terminalRouter.post('/sessions/:id/generate-pr-content', async (req, res) => {
2485
2526
  }).trim();
2486
2527
  // Limit to 3000 chars to avoid token limits
2487
2528
  diffSample = rawDiff.length > 3000 ? rawDiff.substring(0, 3000) + '\n...(truncated)' : rawDiff;
2529
+ // If we have uncommitted changes, append them
2530
+ if (uncommittedChanges) {
2531
+ fileChanges = fileChanges + (fileChanges ? '\n\n=== Uncommitted changes ===\n' : '') + uncommittedChanges;
2532
+ diffSample = diffSample + '\n\n=== Uncommitted changes ===\n(see file stats above)';
2533
+ }
2488
2534
  }
2489
2535
  catch {
2490
- fileChanges = '';
2536
+ // If committed diff failed but we have uncommitted changes, use those
2537
+ if (uncommittedChanges) {
2538
+ fileChanges = uncommittedChanges;
2539
+ }
2540
+ else {
2541
+ fileChanges = '';
2542
+ }
2491
2543
  }
2492
2544
  // If scoped to files but got no results, fall back to unscoped
2493
2545
  if (hasFileScope && !commitMessages && !fileChanges) {
@@ -3132,6 +3184,7 @@ terminalRouter.get('/sessions/:id/ship-summary', (req, res) => {
3132
3184
  console.log(`[ship-summary] Checking for existing PR/MR. Branch: ${currentBranch}, baseBranch: ${baseBranch}, platform: ${creds.platform}, hasToken: ${!!creds.token}, workspaceId: ${workspace?.id}`);
3133
3185
  if (creds.platform === 'github') {
3134
3186
  // Try gh CLI first
3187
+ let ghWorked = false;
3135
3188
  try {
3136
3189
  const prJson = execSync(`gh pr view --json url,number,title,state`, {
3137
3190
  cwd: workingDir,
@@ -3147,10 +3200,130 @@ terminalRouter.get('/sessions/:id/ship-summary', (req, res) => {
3147
3200
  title: pr.title,
3148
3201
  state: pr.state?.toLowerCase() || 'open',
3149
3202
  };
3203
+ ghWorked = true;
3204
+ console.log(`[ship-summary] Found existing PR via gh CLI:`, existingPR);
3150
3205
  }
3151
3206
  }
3152
- catch {
3153
- // No PR exists or gh not available
3207
+ catch (ghErr) {
3208
+ // gh not available or no PR - try API fallback
3209
+ const err = ghErr;
3210
+ console.log(`[ship-summary] gh pr view failed:`, err.message || err.stderr || 'unknown error');
3211
+ }
3212
+ // If gh CLI didn't work, try GitHub API with OAuth token
3213
+ if (!ghWorked && creds.token) {
3214
+ console.log(`[ship-summary] Trying GitHub API fallback with token`);
3215
+ // Helper function to make the API call with a given token
3216
+ const checkPRWithToken = (token) => {
3217
+ try {
3218
+ // Get remote URL to determine repo owner/name
3219
+ const remoteUrl = execSync('git remote get-url origin', {
3220
+ cwd: workingDir,
3221
+ encoding: 'utf-8',
3222
+ stdio: ['pipe', 'pipe', 'pipe'],
3223
+ }).trim();
3224
+ // Parse GitHub repo from remote URL
3225
+ const match = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(?:\.git)?$/);
3226
+ if (!match) {
3227
+ return { found: false, error: 'Could not parse GitHub remote URL' };
3228
+ }
3229
+ const owner = match[1];
3230
+ const repo = match[2].replace(/\.git$/, '');
3231
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls?head=${owner}:${encodeURIComponent(currentBranch)}&state=all`;
3232
+ console.log(`[ship-summary] GitHub API URL: ${apiUrl}`);
3233
+ const tempScriptPath = join(tmpdir(), `github-pr-check-${randomUUID()}.mjs`);
3234
+ const tempResultPath = join(tmpdir(), `github-pr-result-${randomUUID()}.json`);
3235
+ const scriptContent = `
3236
+ import { writeFileSync } from 'fs';
3237
+ try {
3238
+ const response = await fetch(${JSON.stringify(apiUrl)}, {
3239
+ headers: {
3240
+ 'Accept': 'application/vnd.github+json',
3241
+ 'Authorization': 'Bearer ' + ${JSON.stringify(token)},
3242
+ 'X-GitHub-Api-Version': '2022-11-28',
3243
+ 'User-Agent': 'ClaudeDesk',
3244
+ },
3245
+ });
3246
+ const text = await response.text();
3247
+ const status = response.status;
3248
+
3249
+ if (!response.ok) {
3250
+ writeFileSync(${JSON.stringify(tempResultPath)}, JSON.stringify({
3251
+ found: false,
3252
+ status,
3253
+ error: text,
3254
+ }));
3255
+ } else {
3256
+ const data = JSON.parse(text);
3257
+ if (Array.isArray(data) && data.length > 0) {
3258
+ // Get the first PR (most recent)
3259
+ const pr = data[0];
3260
+ writeFileSync(${JSON.stringify(tempResultPath)}, JSON.stringify({
3261
+ found: true,
3262
+ url: pr.html_url,
3263
+ number: pr.number,
3264
+ title: pr.title,
3265
+ state: pr.state,
3266
+ }));
3267
+ } else {
3268
+ writeFileSync(${JSON.stringify(tempResultPath)}, JSON.stringify({
3269
+ found: false,
3270
+ error: 'No PRs found for this branch',
3271
+ }));
3272
+ }
3273
+ }
3274
+ } catch (err) {
3275
+ writeFileSync(${JSON.stringify(tempResultPath)}, JSON.stringify({
3276
+ found: false,
3277
+ error: err.message,
3278
+ }));
3279
+ }
3280
+ `;
3281
+ writeFileSync(tempScriptPath, scriptContent);
3282
+ try {
3283
+ execSync(`node "${tempScriptPath}"`, {
3284
+ cwd: workingDir,
3285
+ encoding: 'utf-8',
3286
+ timeout: 15000,
3287
+ });
3288
+ const resultJson = readFileSync(tempResultPath, 'utf-8');
3289
+ console.log(`[ship-summary] GitHub API result:`, resultJson);
3290
+ return JSON.parse(resultJson);
3291
+ }
3292
+ finally {
3293
+ try {
3294
+ unlinkSync(tempScriptPath);
3295
+ }
3296
+ catch { }
3297
+ try {
3298
+ unlinkSync(tempResultPath);
3299
+ }
3300
+ catch { }
3301
+ }
3302
+ }
3303
+ catch (e) {
3304
+ console.log(`[ship-summary] GitHub API PR check error:`, e);
3305
+ return { found: false, error: String(e) };
3306
+ }
3307
+ };
3308
+ // First attempt with current token
3309
+ let result = checkPRWithToken(creds.token);
3310
+ // If we got a 401, try refreshing the token (for GitHub this would use OAuth refresh)
3311
+ if (result.status === 401 && workspace) {
3312
+ console.log(`[ship-summary] Got 401, token may be expired for workspace ${workspace.id}`);
3313
+ // GitHub OAuth tokens don't expire by default, but if refresh logic is implemented, it would go here
3314
+ }
3315
+ if (result.found) {
3316
+ existingPR = {
3317
+ url: result.url,
3318
+ number: result.number,
3319
+ title: result.title,
3320
+ state: result.state?.toLowerCase() || 'open',
3321
+ };
3322
+ console.log(`[ship-summary] Found existing PR via GitHub API:`, existingPR);
3323
+ }
3324
+ else {
3325
+ console.log(`[ship-summary] No PR found via GitHub API, error:`, result.error);
3326
+ }
3154
3327
  }
3155
3328
  }
3156
3329
  else if (creds.platform === 'gitlab') {
@@ -3332,6 +3505,36 @@ try {
3332
3505
  unpushedCommits = 0;
3333
3506
  }
3334
3507
  }
3508
+ // Check if branch is ahead of base branch (even if pushed)
3509
+ // This allows creating a PR for already-pushed branches
3510
+ let commitsAheadOfBase = 0;
3511
+ if (currentBranch && currentBranch !== baseBranch) {
3512
+ try {
3513
+ // Try origin/baseBranch first (most accurate)
3514
+ const count = execSync(`git rev-list --count origin/${baseBranch}..HEAD`, {
3515
+ cwd: workingDir,
3516
+ encoding: 'utf-8',
3517
+ timeout: 5000,
3518
+ stdio: ['pipe', 'pipe', 'pipe'],
3519
+ }).trim();
3520
+ commitsAheadOfBase = parseInt(count, 10) || 0;
3521
+ }
3522
+ catch {
3523
+ // Fall back to local base branch
3524
+ try {
3525
+ const count = execSync(`git rev-list --count ${baseBranch}..HEAD`, {
3526
+ cwd: workingDir,
3527
+ encoding: 'utf-8',
3528
+ timeout: 5000,
3529
+ stdio: ['pipe', 'pipe', 'pipe'],
3530
+ }).trim();
3531
+ commitsAheadOfBase = parseInt(count, 10) || 0;
3532
+ }
3533
+ catch {
3534
+ commitsAheadOfBase = 0;
3535
+ }
3536
+ }
3537
+ }
3335
3538
  // Get staged and unstaged files
3336
3539
  let hasStagedChanges = false;
3337
3540
  let hasUnstagedChanges = false;
@@ -3597,7 +3800,7 @@ try {
3597
3800
  }
3598
3801
  }
3599
3802
  const hasUncommittedChanges = hasStagedChanges || hasUnstagedChanges;
3600
- const hasChangesToShip = files.length > 0 || unpushedCommits > 0;
3803
+ const hasChangesToShip = files.length > 0 || unpushedCommits > 0 || commitsAheadOfBase > 0;
3601
3804
  res.json({
3602
3805
  success: true,
3603
3806
  data: {
@@ -3609,6 +3812,7 @@ try {
3609
3812
  hasUncommittedChanges,
3610
3813
  hasChangesToShip,
3611
3814
  unpushedCommits,
3815
+ commitsAheadOfBase,
3612
3816
  hasStagedChanges,
3613
3817
  hasUnstagedChanges,
3614
3818
  existingPR,
@@ -3894,6 +4098,113 @@ terminalRouter.post('/usage/quota/refresh', async (_req, res) => {
3894
4098
  }
3895
4099
  });
3896
4100
  // ============================================================================
4101
+ // Budget Allocator Endpoints
4102
+ // ============================================================================
4103
+ // Get allocator config
4104
+ terminalRouter.get('/usage/budget-config', (_req, res) => {
4105
+ try {
4106
+ const config = allocatorManager.getConfig();
4107
+ res.json({ success: true, data: config });
4108
+ }
4109
+ catch (error) {
4110
+ const errorMsg = error instanceof Error ? error.message : String(error);
4111
+ res.status(500).json({ success: false, error: errorMsg });
4112
+ }
4113
+ });
4114
+ // Update allocator config
4115
+ terminalRouter.put('/usage/budget-config', (req, res) => {
4116
+ try {
4117
+ const config = allocatorManager.updateConfig(req.body);
4118
+ res.json({ success: true, data: config });
4119
+ }
4120
+ catch (error) {
4121
+ const errorMsg = error instanceof Error ? error.message : String(error);
4122
+ res.status(500).json({ success: false, error: errorMsg });
4123
+ }
4124
+ });
4125
+ // Reset allocator config to defaults
4126
+ terminalRouter.post('/usage/budget-config/reset', (_req, res) => {
4127
+ try {
4128
+ const config = allocatorManager.resetConfig();
4129
+ res.json({ success: true, data: config });
4130
+ }
4131
+ catch (error) {
4132
+ const errorMsg = error instanceof Error ? error.message : String(error);
4133
+ res.status(500).json({ success: false, error: errorMsg });
4134
+ }
4135
+ });
4136
+ // Get burn rate + projection
4137
+ terminalRouter.get('/usage/burn-rate', (_req, res) => {
4138
+ try {
4139
+ const burnRate = allocatorManager.getBurnRate();
4140
+ res.json({ success: true, data: burnRate });
4141
+ }
4142
+ catch (error) {
4143
+ const errorMsg = error instanceof Error ? error.message : String(error);
4144
+ res.status(500).json({ success: false, error: errorMsg });
4145
+ }
4146
+ });
4147
+ // Get utilization history for chart
4148
+ terminalRouter.get('/usage/history', (_req, res) => {
4149
+ try {
4150
+ const history = allocatorManager.getUtilizationHistory();
4151
+ res.json({ success: true, data: history });
4152
+ }
4153
+ catch (error) {
4154
+ const errorMsg = error instanceof Error ? error.message : String(error);
4155
+ res.status(500).json({ success: false, error: errorMsg });
4156
+ }
4157
+ });
4158
+ // Estimate cost for next message
4159
+ terminalRouter.post('/usage/estimate', (req, res) => {
4160
+ try {
4161
+ const { sessionId } = req.body || {};
4162
+ const estimate = allocatorManager.estimateMessageCost(sessionId);
4163
+ res.json({ success: true, data: estimate });
4164
+ }
4165
+ catch (error) {
4166
+ const errorMsg = error instanceof Error ? error.message : String(error);
4167
+ res.status(500).json({ success: false, error: errorMsg });
4168
+ }
4169
+ });
4170
+ // Check budget limits before sending
4171
+ terminalRouter.post('/usage/check-budget', (req, res) => {
4172
+ try {
4173
+ const { fiveHour, sevenDay } = req.body || {};
4174
+ const result = allocatorManager.checkBudgetLimits(fiveHour !== undefined && sevenDay !== undefined
4175
+ ? { fiveHour, sevenDay }
4176
+ : undefined);
4177
+ res.json({ success: true, data: result });
4178
+ }
4179
+ catch (error) {
4180
+ const errorMsg = error instanceof Error ? error.message : String(error);
4181
+ res.status(500).json({ success: false, error: errorMsg });
4182
+ }
4183
+ });
4184
+ // Estimate queue batch cost
4185
+ terminalRouter.post('/usage/estimate-queue', (req, res) => {
4186
+ try {
4187
+ const { messageCount } = req.body || {};
4188
+ const estimate = allocatorManager.estimateQueueCost(messageCount || 1);
4189
+ res.json({ success: true, data: estimate });
4190
+ }
4191
+ catch (error) {
4192
+ const errorMsg = error instanceof Error ? error.message : String(error);
4193
+ res.status(500).json({ success: false, error: errorMsg });
4194
+ }
4195
+ });
4196
+ // Record a utilization sample (called by frontend or cron)
4197
+ terminalRouter.post('/usage/sample', async (_req, res) => {
4198
+ try {
4199
+ await allocatorManager.recordUtilizationSample();
4200
+ res.json({ success: true });
4201
+ }
4202
+ catch (error) {
4203
+ const errorMsg = error instanceof Error ? error.message : String(error);
4204
+ res.status(500).json({ success: false, error: errorMsg });
4205
+ }
4206
+ });
4207
+ // ============================================================================
3897
4208
  // Context Management Endpoints
3898
4209
  // ============================================================================
3899
4210
  // Get context state for a session