@rikcodes/teamclaude 1.1.19-rik.1 → 1.1.19-rik.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rikcodes/teamclaude",
3
- "version": "1.1.19-rik.1",
3
+ "version": "1.1.19-rik.2",
4
4
  "description": "Multi-account proxy for Claude Code and Codex: pools Claude Max, ChatGPT/Codex, API-key and third-party backend accounts, and rotates on quota",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/claude-env.js CHANGED
@@ -26,11 +26,50 @@ export function validPort(port) {
26
26
  return n;
27
27
  }
28
28
 
29
+ // The loopback entries every launched client gets. They keep the client's own
30
+ // localhost traffic out of the proxy: a forward to loopback is refused
31
+ // (forward-target.js), so a client that proxied it would get a 403 instead of
32
+ // its own dev server.
33
+ export const LOOPBACK_NO_PROXY = ['localhost', '127.0.0.1', '::1'];
34
+
35
+ /** True if `value` names `*` — "proxy nothing" — among its entries. */
36
+ export function bypassesAllHosts(value) {
37
+ return String(value ?? '').split(',').some((entry) => entry.trim() === '*');
38
+ }
39
+
40
+ /**
41
+ * The NO_PROXY a launched client gets: ours, plus whatever the operator had.
42
+ *
43
+ * Replacing theirs broke the case the list exists for. A local dev host is
44
+ * rarely spelled `localhost` — `*.test` and friends resolve to 127.0.0.1
45
+ * through a local resolver — so with only our three entries the client proxies
46
+ * it, the proxy refuses the loopback forward, and the client retries the 403 on
47
+ * a loop. Their entries are kept verbatim (a leading dot or a `host:port` is
48
+ * theirs to mean), deduped case-insensitively, ours first.
49
+ *
50
+ * `*` is the one entry dropped: it routes every host around the proxy,
51
+ * api.anthropic.com included, which silently turns the launch into a direct run
52
+ * — no rotation, the operator's own quota. `--no-mitm` is how that is asked for.
53
+ */
54
+ export function mergeNoProxy(...inherited) {
55
+ const seen = new Set();
56
+ const out = [];
57
+ const entries = inherited.flatMap((value) => String(value ?? '').split(','));
58
+ for (const entry of [...LOOPBACK_NO_PROXY, ...entries]) {
59
+ const host = entry.trim();
60
+ if (!host || host === '*' || seen.has(host.toLowerCase())) continue;
61
+ seen.add(host.toLowerCase());
62
+ out.push(host);
63
+ }
64
+ return out.join(',');
65
+ }
66
+
29
67
  // Build the shell `export` lines that point Claude Code — or any tool that
30
68
  // spawns it, e.g. an agent multiplexer — at the proxy. This is the same
31
69
  // environment `teamclaude run` sets up, but emitted for `eval "$(teamclaude
32
70
  // env)"` instead of launching claude directly. Pure and side-effect free so it
33
- // can be unit-tested; the caller resolves the port, cert path, and holdSeconds.
71
+ // can be unit-tested; the caller resolves the port, cert path, holdSeconds and
72
+ // the NO_PROXY the invoking shell already had.
34
73
  //
35
74
  // MITM (forward-proxy) mode is the default, matching `teamclaude run`: it routes
36
75
  // ALL of claude's traffic through the proxy — even hardcoded api.anthropic.com
@@ -101,7 +140,7 @@ export function buildCustomModelVars(customModels) {
101
140
  return vars;
102
141
  }
103
142
 
104
- export function buildClaudeEnvLines({ port, useMitm = true, caPath = null, holdSeconds = 0, account = null, proxyApiKey = '', customModels = null }) {
143
+ export function buildClaudeEnvLines({ port, useMitm = true, caPath = null, holdSeconds = 0, account = null, proxyApiKey = '', customModels = null, inheritedNoProxy = null }) {
105
144
  const lines = [];
106
145
  const pin = (account || '').trim();
107
146
  // The port is interpolated unquoted into URLs the shell evals, so it has to
@@ -111,13 +150,16 @@ export function buildClaudeEnvLines({ port, useMitm = true, caPath = null, holdS
111
150
  if (useMitm) {
112
151
  const userinfo = pin ? `${encodePinComponent(pin)}:${encodePinComponent(proxyApiKey || '')}@` : '';
113
152
  const proxyUrl = `http://${userinfo}127.0.0.1:${port}`;
153
+ const noProxy = mergeNoProxy(inheritedNoProxy);
114
154
  lines.push(
115
155
  `export HTTPS_PROXY=${proxyUrl}`,
116
156
  `export HTTP_PROXY=${proxyUrl}`,
117
157
  `export https_proxy=${proxyUrl}`,
118
158
  `export http_proxy=${proxyUrl}`,
119
- 'export NO_PROXY=localhost,127.0.0.1,::1',
120
- 'export no_proxy=localhost,127.0.0.1,::1',
159
+ // Quoted like the CA path below: the value now carries whatever the
160
+ // operator's own NO_PROXY held, and this line is eval'd.
161
+ `export NO_PROXY=${shellQuote(noProxy)}`,
162
+ `export no_proxy=${shellQuote(noProxy)}`,
121
163
  );
122
164
  // Quoted: the path is under $HOME (or XDG_CONFIG_HOME), which can carry a
123
165
  // space or a quote, and this line is eval'd.
package/src/index.js CHANGED
@@ -39,7 +39,7 @@ import { autoUpdate, checkForUpdate, currentVersion, runUpdate, installKind, PKG
39
39
  import { renderStatus, formatPercent } from './status-renderer.js';
40
40
  import { sanitizeText } from './safe-text.js';
41
41
  import { ClientUsageTracker, UsageDimensionTracker } from './client-usage.js';
42
- import { buildClaudeEnvLines, buildCustomModelAgents, buildCustomModelSettings, buildCustomModelVars, encodePinComponent } from './claude-env.js';
42
+ import { buildClaudeEnvLines, buildCustomModelAgents, buildCustomModelSettings, buildCustomModelVars, bypassesAllHosts, encodePinComponent, mergeNoProxy } from './claude-env.js';
43
43
  import { serviceKind, installService, uninstallService, serviceStatus, renderService, logPath } from './service.js';
44
44
  import { formatTerminalTitle, titleSequence, TITLE_STACK_PUSH, TITLE_STACK_POP } from './terminal-title.js';
45
45
  import { getUpstreamProxy, describeProxy, describeSelfProxy } from './upstream-proxy.js';
@@ -953,6 +953,9 @@ async function envCommand() {
953
953
  port, useMitm, caPath, holdSeconds: config.holdSeconds,
954
954
  account, proxyApiKey: config.proxy?.apiKey || '',
955
955
  customModels: config.customModels,
956
+ // The shell doing the eval keeps its own NO_PROXY entries; re-running is
957
+ // idempotent, since the merged value is what it will have next time.
958
+ inheritedNoProxy: [process.env.NO_PROXY, process.env.no_proxy].filter(Boolean).join(','),
956
959
  });
957
960
  } catch (err) {
958
961
  // A bad proxy.port. Nothing reaches stdout: the shell is eval'ing it.
@@ -1032,7 +1035,13 @@ async function runCommand() {
1032
1035
  : '';
1033
1036
  const proxyUrl = `http://${userinfo}127.0.0.1:${port}`;
1034
1037
  env.HTTPS_PROXY = env.HTTP_PROXY = env.https_proxy = env.http_proxy = proxyUrl;
1035
- env.NO_PROXY = env.no_proxy = 'localhost,127.0.0.1,::1';
1038
+ // Keep the operator's own NO_PROXY and add ours — see mergeNoProxy. Both
1039
+ // spellings are read: a tool that set only one still meant it.
1040
+ const inheritedNoProxy = [process.env.NO_PROXY, process.env.no_proxy];
1041
+ env.NO_PROXY = env.no_proxy = mergeNoProxy(...inheritedNoProxy);
1042
+ if (inheritedNoProxy.some(bypassesAllHosts)) {
1043
+ console.error('[TeamClaude] NO_PROXY=* ignored: it would send api.anthropic.com around the proxy (no rotation). Use --no-mitm for a direct launch.');
1044
+ }
1036
1045
  env.NODE_EXTRA_CA_CERTS = caPath;
1037
1046
  if (tcAcct) console.error(`[TeamClaude] Pinned to account "${tcAcct}" (TC_ACCT)`);
1038
1047
  else if (pinnedBase) {