polydev-ai 1.9.18 → 1.9.20

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.
package/mcp/manifest.json CHANGED
@@ -98,6 +98,10 @@
98
98
  "minimum": 30,
99
99
  "maximum": 600,
100
100
  "default": 300
101
+ },
102
+ "user_token": {
103
+ "type": "string",
104
+ "description": "Polydev user token (pd_xxx). Use this in sandboxed environments (Cowork, containers) where browser login is unavailable. Get your token from https://polydev.ai/dashboard"
101
105
  }
102
106
  }
103
107
  },
@@ -300,6 +300,9 @@ class StdioMCPWrapper {
300
300
  // Pending session file for surviving restarts
301
301
  this.PENDING_SESSION_FILE = path.join(os.homedir(), '.polydev-pending-session');
302
302
 
303
+ // Sandbox detection (Cowork, containers with limited disk/no browser)
304
+ this._isSandboxed = null; // lazy-detected
305
+
303
306
  // MCP client info (set during initialize handshake)
304
307
  // Used to detect which IDE is running us and exclude its CLI to avoid recursive calls
305
308
  this.clientInfo = null;
@@ -491,7 +494,18 @@ Token will be saved automatically after login.`
491
494
  */
492
495
  async handleLoginTool(params, id) {
493
496
  const crypto = require('crypto');
494
-
497
+
498
+ // Accept user_token from params (essential for sandboxed environments like Cowork)
499
+ const inlineToken = params?.arguments?.user_token;
500
+ if (inlineToken && (inlineToken.startsWith('pd_') || inlineToken.startsWith('polydev_'))) {
501
+ console.error('[Polydev] Login via inline user_token');
502
+ this.userToken = inlineToken;
503
+ this.isAuthenticated = true;
504
+ process.env.POLYDEV_USER_TOKEN = inlineToken;
505
+ this.saveTokenToFiles(inlineToken);
506
+ return await this.fetchAuthStatusResponse(id);
507
+ }
508
+
495
509
  // Check if already authenticated - verify with server first
496
510
  if (this.isAuthenticated && this.userToken) {
497
511
  console.error('[Polydev] Token exists locally, verifying with server...');
@@ -525,9 +539,38 @@ To re-login: npx polydev-ai`
525
539
  this.userToken = null;
526
540
  }
527
541
 
542
+ // In sandboxed environments, don't try browser login — show token-paste instructions
543
+ if (this.isSandboxed()) {
544
+ console.error('[Polydev] Sandbox detected — browser login unavailable');
545
+ return {
546
+ jsonrpc: '2.0',
547
+ id,
548
+ result: {
549
+ content: [{
550
+ type: 'text',
551
+ text: `LOGIN - Sandboxed Environment
552
+ ==============================
553
+
554
+ Browser login is not available in this environment (Cowork/container).
555
+
556
+ To authenticate, use one of these methods:
557
+
558
+ **Method 1: Pass token directly**
559
+ 1. Get your token from https://polydev.ai/dashboard
560
+ 2. Call login with user_token:
561
+
562
+ mcp__plugin_polydev_mcp__login({ user_token: "pd_your_token_here" })
563
+
564
+ **Method 2: Set environment variable**
565
+ Set POLYDEV_USER_TOKEN=pd_your_token before starting the MCP server.`
566
+ }]
567
+ }
568
+ };
569
+ }
570
+
528
571
  // Generate unique session ID for polling-based auth
529
572
  const sessionId = crypto.randomBytes(32).toString('hex');
530
-
573
+
531
574
  try {
532
575
  // Create session on server (with retry for transient network issues)
533
576
  let createResponse;
@@ -845,14 +888,16 @@ Still waiting for login. Use /polydev:auth to check status after signing in.`
845
888
  }
846
889
  };
847
890
  } catch (error) {
848
- const cause = error.cause ? ` (${error.cause.code || error.cause.message || error.cause})` : '';
891
+ const errCode = error.cause?.code || error.code || '';
892
+ const cause = errCode ? ` (${errCode})` : (error.cause?.message ? ` (${error.cause.message})` : '');
893
+ console.error(`[Polydev] Re-auth failed: ${error.message}${cause}`);
849
894
  return {
850
895
  jsonrpc: '2.0',
851
896
  id,
852
897
  result: {
853
898
  content: [{
854
899
  type: 'text',
855
- text: `${reason}\n\nCould not reach server: ${error.message}${cause}\nPlease run: npx polydev-ai`
900
+ text: `${reason}\n\nCould not reach Polydev server: ${error.message}${cause}\n\nTroubleshooting:\n 1. Check your internet connection\n 2. Try: /polydev:login\n 3. Or run in terminal: npx polydev-ai`
856
901
  }],
857
902
  isError: true
858
903
  }
@@ -934,31 +979,54 @@ Still waiting for login. Use /polydev:auth to check status after signing in.`
934
979
  * Handle get_auth_status tool - comprehensive status display
935
980
  */
936
981
  async handleGetAuthStatus(params, id) {
982
+ // Accept user_token from params (essential for sandboxed environments like Cowork)
983
+ const inlineToken = params?.arguments?.user_token;
984
+ if (inlineToken && (inlineToken.startsWith('pd_') || inlineToken.startsWith('polydev_'))) {
985
+ console.error('[Polydev] Using inline user_token from params');
986
+ this.userToken = inlineToken;
987
+ this.isAuthenticated = true;
988
+ process.env.POLYDEV_USER_TOKEN = inlineToken;
989
+ this.saveTokenToFiles(inlineToken); // safe — handles sandbox gracefully
990
+ }
991
+
937
992
  // Try to hot-reload token if not authenticated
938
993
  if (!this.isAuthenticated || !this.userToken) {
939
994
  this.reloadTokenFromFiles();
940
995
  }
941
-
996
+
942
997
  if (!this.isAuthenticated || !this.userToken) {
943
- return {
944
- jsonrpc: '2.0',
945
- id,
946
- result: {
947
- content: [{
948
- type: 'text',
949
- text: `POLYDEV STATUS
998
+ // No token found — check if we're in a sandbox (Cowork, containers)
999
+ if (this.isSandboxed()) {
1000
+ console.error('[Polydev] No token and sandboxed — showing token-paste instructions');
1001
+ return {
1002
+ jsonrpc: '2.0',
1003
+ id,
1004
+ result: {
1005
+ content: [{
1006
+ type: 'text',
1007
+ text: `POLYDEV STATUS
950
1008
  ==============
951
1009
 
952
- Authentication: Not connected
1010
+ Authentication: Not connected (sandboxed environment detected)
953
1011
 
954
- To login:
955
- 1. Use the "login" tool (opens browser)
956
- 2. Or run: npx polydev-ai
1012
+ Browser login is not available in this environment.
957
1013
 
958
- Token will be saved automatically after login.`
959
- }]
960
- }
961
- };
1014
+ To authenticate, pass your token directly:
1015
+ 1. Get your token from https://polydev.ai/dashboard
1016
+ 2. Call get_auth_status with user_token parameter:
1017
+
1018
+ mcp__plugin_polydev_mcp__get_auth_status({ user_token: "pd_your_token_here" })
1019
+
1020
+ 3. Or set the environment variable before starting:
1021
+ POLYDEV_USER_TOKEN=pd_your_token_here`
1022
+ }]
1023
+ }
1024
+ };
1025
+ }
1026
+
1027
+ // Not sandboxed — auto-trigger browser login
1028
+ console.error('[Polydev] No token found, auto-triggering login flow...');
1029
+ return await this.triggerReAuth(id, 'Not authenticated. Opening browser for login...');
962
1030
  }
963
1031
 
964
1032
  try {
@@ -1053,6 +1121,16 @@ Configure: https://polydev.ai/dashboard/models`
1053
1121
  return await this.triggerReAuth(id, 'Token invalid or expired.');
1054
1122
  }
1055
1123
  } catch (error) {
1124
+ // Categorize the error for clearer user messaging
1125
+ const errCode = error.cause?.code || error.code || '';
1126
+ const isNetwork = ['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNRESET', 'UND_ERR_CONNECT_TIMEOUT', 'FETCH_ERROR'].includes(errCode)
1127
+ || error.message?.includes('fetch') || error.message?.includes('network');
1128
+ const errorDetail = isNetwork
1129
+ ? `Network error: ${error.message}${errCode ? ` (${errCode})` : ''}\nCheck your internet connection and try again.`
1130
+ : `Error: ${error.message}${errCode ? ` (${errCode})` : ''}`;
1131
+
1132
+ console.error(`[Polydev] Auth status check failed: ${error.message} (code: ${errCode})`);
1133
+
1056
1134
  return {
1057
1135
  jsonrpc: '2.0',
1058
1136
  id,
@@ -1062,9 +1140,12 @@ Configure: https://polydev.ai/dashboard/models`
1062
1140
  text: `POLYDEV STATUS
1063
1141
  ==============
1064
1142
 
1065
- Could not verify status (offline?)
1143
+ Could not verify authentication status.
1144
+
1145
+ ${errorDetail}
1066
1146
 
1067
- Error: ${error.message}`
1147
+ To retry: /polydev:auth
1148
+ To re-login: /polydev:login`
1068
1149
  }],
1069
1150
  isError: true
1070
1151
  }
@@ -1072,6 +1153,29 @@ Error: ${error.message}`
1072
1153
  }
1073
1154
  }
1074
1155
 
1156
+ /**
1157
+ * Detect if running in a sandboxed environment (Cowork, containers, etc.)
1158
+ * where filesystem is read-only/limited and browser is unavailable.
1159
+ * Result is cached after first check.
1160
+ */
1161
+ isSandboxed() {
1162
+ if (this._isSandboxed !== null) return this._isSandboxed;
1163
+
1164
+ try {
1165
+ // Try writing a test file to home directory
1166
+ const testFile = path.join(os.homedir(), '.polydev-sandbox-test');
1167
+ fs.writeFileSync(testFile, 'test');
1168
+ fs.unlinkSync(testFile);
1169
+ this._isSandboxed = false;
1170
+ } catch (e) {
1171
+ // ENOSPC, EROFS, EACCES all indicate sandbox-like constraints
1172
+ console.error(`[Polydev] Sandbox detected: ${e.code || e.message}`);
1173
+ this._isSandboxed = true;
1174
+ }
1175
+
1176
+ return this._isSandboxed;
1177
+ }
1178
+
1075
1179
  /**
1076
1180
  * Open URL in browser - best-in-class cross-platform implementation
1077
1181
  * Uses 'open' npm package with fallbacks for reliability in MCP/stdio context
@@ -1134,27 +1238,39 @@ Error: ${error.message}`
1134
1238
  * Save token to shell config files
1135
1239
  */
1136
1240
  saveTokenToFiles(token) {
1137
- const shell = process.env.SHELL || '/bin/zsh';
1138
- const rcFile = shell.includes('zsh') ? path.join(os.homedir(), '.zshrc') :
1139
- shell.includes('bash') ? path.join(os.homedir(), '.bashrc') :
1140
- path.join(os.homedir(), '.profile');
1241
+ if (this.isSandboxed()) {
1242
+ // In sandbox, just set in-memory — can't write files
1243
+ console.error('[Polydev] Sandbox: skipping file writes, token set in-memory only');
1244
+ process.env.POLYDEV_USER_TOKEN = token;
1245
+ return;
1246
+ }
1141
1247
 
1142
- let content = '';
1143
- try { content = fs.readFileSync(rcFile, 'utf8'); } catch (e) {}
1248
+ try {
1249
+ const shell = process.env.SHELL || '/bin/zsh';
1250
+ const rcFile = shell.includes('zsh') ? path.join(os.homedir(), '.zshrc') :
1251
+ shell.includes('bash') ? path.join(os.homedir(), '.bashrc') :
1252
+ path.join(os.homedir(), '.profile');
1253
+
1254
+ let content = '';
1255
+ try { content = fs.readFileSync(rcFile, 'utf8'); } catch (e) {}
1144
1256
 
1145
- const lines = content.split('\n').filter(line =>
1146
- !line.trim().startsWith('export POLYDEV_USER_TOKEN') &&
1147
- !line.trim().startsWith('POLYDEV_USER_TOKEN')
1148
- );
1257
+ const lines = content.split('\n').filter(line =>
1258
+ !line.trim().startsWith('export POLYDEV_USER_TOKEN') &&
1259
+ !line.trim().startsWith('POLYDEV_USER_TOKEN')
1260
+ );
1149
1261
 
1150
- lines.push(`export POLYDEV_USER_TOKEN="${token}"`);
1151
- fs.writeFileSync(rcFile, lines.join('\n').replace(/\n+$/, '') + '\n');
1262
+ lines.push(`export POLYDEV_USER_TOKEN="${token}"`);
1263
+ fs.writeFileSync(rcFile, lines.join('\n').replace(/\n+$/, '') + '\n');
1152
1264
 
1153
- // Also save to .polydev.env
1154
- const envFile = path.join(os.homedir(), '.polydev.env');
1155
- fs.writeFileSync(envFile, `POLYDEV_USER_TOKEN="${token}"\n`);
1265
+ // Also save to .polydev.env
1266
+ const envFile = path.join(os.homedir(), '.polydev.env');
1267
+ fs.writeFileSync(envFile, `POLYDEV_USER_TOKEN="${token}"\n`);
1156
1268
 
1157
- console.error(`[Polydev] Token saved to ${rcFile} and ${envFile}`);
1269
+ console.error(`[Polydev] Token saved to ${rcFile} and ${envFile}`);
1270
+ } catch (e) {
1271
+ console.error(`[Polydev] Could not write token to files (${e.code || e.message}), set in-memory only`);
1272
+ process.env.POLYDEV_USER_TOKEN = token;
1273
+ }
1158
1274
  }
1159
1275
 
1160
1276
  /**
@@ -1831,7 +1947,7 @@ Error: ${error.message}`
1831
1947
  // Detect if we should exclude the current IDE's CLI to avoid recursive calls
1832
1948
  const excludedCli = this.getExcludedCliForCurrentIDE();
1833
1949
  if (excludedCli) {
1834
- console.error(`[Stdio Wrapper] Excluding CLI '${excludedCli}' (current IDE: ${this.clientInfo?.name}) to avoid recursive calls`);
1950
+ console.error(`[Stdio Wrapper] [CLI-FIRST] Skipping ${excludedCli} (same as current IDE would cause recursive call)`);
1835
1951
  }
1836
1952
 
1837
1953
  // Build merged provider list: CLIs first, then API-only
@@ -1901,7 +2017,7 @@ Error: ${error.message}`
1901
2017
  console.error(`[Stdio Wrapper] Provider breakdown: CLI=${cliProviderEntries.map(p => p.cliId).join(', ') || 'none'}, API-only=${apiOnlyProviders.map(p => p.provider).join(', ') || 'none'}`);
1902
2018
 
1903
2019
  // Run ALL CLI prompts concurrently with fast-collect pattern
1904
- // Resolves once we have maxPerspectives successes (don't wait for slow CLIs)
2020
+ // Resolves once we have maxPerspectives successes OR all complete
1905
2021
  if (cliProviderEntries.length > 0) {
1906
2022
  const cliPromises = cliProviderEntries.map(async (providerEntry) => {
1907
2023
  try {
@@ -1953,7 +2069,7 @@ Error: ${error.message}`
1953
2069
  }
1954
2070
  });
1955
2071
 
1956
- // Fast-collect: resolve once we have maxPerspectives successes OR all complete
2072
+ // Fast-collect: resolve early once we have maxPerspectives successes OR all complete
1957
2073
  localResults = await this.collectFirstNSuccesses(cliPromises, maxPerspectives);
1958
2074
  console.error(`[Stdio Wrapper] Fast-collect: got ${localResults.filter(r => r.success).length} successful, ${localResults.filter(r => !r.success).length} failed out of ${cliPromises.length} CLIs`);
1959
2075
  }
@@ -2664,7 +2780,7 @@ Error: ${error.message}`
2664
2780
  const staleProviders = [];
2665
2781
  for (const [providerId, status] of Object.entries(currentStatus)) {
2666
2782
  if (this.isStale(status)) {
2667
- const minutesOld = Math.floor((new Date().getTime() - new Date(status.last_checked).getTime()) / (1000 * 60));
2783
+ const minutesOld = Math.floor((now.getTime() - lastChecked.getTime()) / (1000 * 60));
2668
2784
  const timeout = this.getSmartTimeout(status);
2669
2785
  staleProviders.push({ providerId, minutesOld, timeout });
2670
2786
  }
@@ -3153,16 +3269,18 @@ Dashboard: https://polydev.ai/dashboard`
3153
3269
  console.error(`Credits: ${credits} | Tier: ${tier}`);
3154
3270
  console.error('─'.repeat(50) + '\n');
3155
3271
  } else {
3156
- this.isAuthenticated = false;
3272
+ // Don't clear isAuthenticated here — handleGetAuthStatus has its own
3273
+ // verification and triggerReAuth logic. Clearing here causes a race condition
3274
+ // where startup verification can invalidate auth before the user checks status.
3157
3275
  console.error('─'.repeat(50));
3158
- console.error('Polydev - Token Invalid');
3276
+ console.error('Polydev - Token may be invalid');
3159
3277
  console.error('─'.repeat(50));
3160
- console.error('Your token may have expired.');
3278
+ console.error('Server returned non-OK. Will re-verify on next auth check.');
3161
3279
  console.error('Use the "login" tool or run: npx polydev-ai login');
3162
3280
  console.error('─'.repeat(50) + '\n');
3163
3281
  }
3164
3282
  } catch (error) {
3165
- console.error('[Polydev] Could not verify auth (offline?)');
3283
+ console.error('[Polydev] Could not verify auth (offline?):', error.message || 'unknown');
3166
3284
  }
3167
3285
  }
3168
3286
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydev-ai",
3
- "version": "1.9.18",
3
+ "version": "1.9.20",
4
4
  "engines": {
5
5
  "node": ">=20.x <=22.x"
6
6
  },