claude-cac 1.4.0 → 1.4.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/cac +7 -2
- package/cac-dns-guard.js +326 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +34 -22
package/cac
CHANGED
|
@@ -11,7 +11,7 @@ VERSIONS_DIR="$CAC_DIR/versions"
|
|
|
11
11
|
# ── utils: colors, read/write, UUID, proxy parsing ───────────────────────
|
|
12
12
|
|
|
13
13
|
# shellcheck disable=SC2034 # used in build-concatenated cac script
|
|
14
|
-
CAC_VERSION="1.4.
|
|
14
|
+
CAC_VERSION="1.4.2"
|
|
15
15
|
|
|
16
16
|
_read() { [[ -f "$1" ]] && tr -d '[:space:]' < "$1" || echo "${2:-}"; }
|
|
17
17
|
_die() { printf '%b\n' "$(_red "error:") $*" >&2; exit 1; }
|
|
@@ -1356,15 +1356,20 @@ fi
|
|
|
1356
1356
|
# ── Concurrent session check ──
|
|
1357
1357
|
_max_sessions=10
|
|
1358
1358
|
[[ -f "$CAC_DIR/max_sessions" ]] && _ms=$(tr -d '[:space:]' < "$CAC_DIR/max_sessions") && [[ -n "$_ms" ]] && _max_sessions="$_ms"
|
|
1359
|
-
|
|
1359
|
+
# pgrep exits 1 when no match; with pipefail + set -e that would abort the wrapper
|
|
1360
|
+
_claude_count=$(pgrep -x "claude" 2>/dev/null | wc -l | tr -d '[:space:]') || _claude_count=0
|
|
1360
1361
|
if [[ "$_claude_count" -gt "$_max_sessions" ]]; then
|
|
1361
1362
|
echo "[cac] warning: $_claude_count claude sessions running (threshold: $_max_sessions)" >&2
|
|
1362
1363
|
echo "[cac] hint: concurrent sessions on the same device may trigger detection" >&2
|
|
1363
1364
|
echo "[cac] hint: adjust threshold with: echo '{\"max_sessions\": 20}' > ~/.cac/settings.json" >&2
|
|
1364
1365
|
fi
|
|
1365
1366
|
|
|
1367
|
+
# claude non-zero exit must not leave _ec unset (set -u) or abort before cleanup (set -e)
|
|
1368
|
+
_ec=0
|
|
1369
|
+
set +e
|
|
1366
1370
|
"$_real" "$@"
|
|
1367
1371
|
_ec=$?
|
|
1372
|
+
set -e
|
|
1368
1373
|
_cleanup_all
|
|
1369
1374
|
exit "$_ec"
|
|
1370
1375
|
WRAPPER_EOF
|
package/cac-dns-guard.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// cac-dns-guard.js
|
|
3
|
+
// DNS-level telemetry domain blocking | mTLS certificate injection | fetch leak prevention
|
|
4
|
+
// Injected into Claude Code process via NODE_OPTIONS="--require <this>"
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
var dns = require('dns');
|
|
9
|
+
var net = require('net');
|
|
10
|
+
var tls = require('tls');
|
|
11
|
+
var http = require('http');
|
|
12
|
+
var https = require('https');
|
|
13
|
+
var fs = require('fs');
|
|
14
|
+
|
|
15
|
+
// ─── 1. DNS-level telemetry domain blocking ──────────────────────────────────
|
|
16
|
+
|
|
17
|
+
var BLOCKED_DOMAINS = new Set([
|
|
18
|
+
'statsig.anthropic.com',
|
|
19
|
+
'sentry.io',
|
|
20
|
+
'o1137031.ingest.sentry.io',
|
|
21
|
+
'cdn.growthbook.io',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if domain is in block list (including subdomain matching)
|
|
26
|
+
* e.g. "foo.sentry.io" matches "sentry.io"
|
|
27
|
+
*/
|
|
28
|
+
function isDomainBlocked(hostname) {
|
|
29
|
+
if (!hostname) return false;
|
|
30
|
+
var h = hostname.toLowerCase().replace(/\.$/,'');
|
|
31
|
+
if (BLOCKED_DOMAINS.has(h)) return true;
|
|
32
|
+
var parts = h.split('.');
|
|
33
|
+
for (var i = 1; i < parts.length - 1; i++) {
|
|
34
|
+
if (BLOCKED_DOMAINS.has(parts.slice(i).join('.'))) return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeBlockedError(hostname, syscall) {
|
|
40
|
+
var msg = 'connect ECONNREFUSED (blocked by cac): ' + hostname;
|
|
41
|
+
var err = new Error(msg);
|
|
42
|
+
err.code = 'ECONNREFUSED';
|
|
43
|
+
err.errno = -111;
|
|
44
|
+
err.hostname = hostname;
|
|
45
|
+
err.syscall = syscall || 'connect';
|
|
46
|
+
return err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 1a. intercept dns.lookup ──
|
|
50
|
+
var _origLookup = dns.lookup;
|
|
51
|
+
dns.lookup = function cacLookup(hostname, options, callback) {
|
|
52
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
53
|
+
if (isDomainBlocked(hostname)) {
|
|
54
|
+
var err = makeBlockedError(hostname, 'getaddrinfo');
|
|
55
|
+
if (typeof callback === 'function') process.nextTick(function() { callback(err); });
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
return _origLookup.call(dns, hostname, options, callback);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ── 1b. intercept dns.resolve / resolve4 / resolve6 ──
|
|
62
|
+
['resolve','resolve4','resolve6'].forEach(function(method) {
|
|
63
|
+
var orig = dns[method];
|
|
64
|
+
if (!orig) return;
|
|
65
|
+
dns[method] = function(hostname) {
|
|
66
|
+
var args = Array.prototype.slice.call(arguments);
|
|
67
|
+
var cb = args[args.length - 1];
|
|
68
|
+
if (isDomainBlocked(hostname)) {
|
|
69
|
+
var err = makeBlockedError(hostname, 'query');
|
|
70
|
+
if (typeof cb === 'function') process.nextTick(function() { cb(err); });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
return orig.apply(dns, args);
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── 1c. intercept dns.promises ──
|
|
78
|
+
if (dns.promises) {
|
|
79
|
+
var _origPLookup = dns.promises.lookup;
|
|
80
|
+
if (_origPLookup) {
|
|
81
|
+
dns.promises.lookup = function cacPromiseLookup(hostname, options) {
|
|
82
|
+
if (isDomainBlocked(hostname)) return Promise.reject(makeBlockedError(hostname, 'getaddrinfo'));
|
|
83
|
+
return _origPLookup.call(dns.promises, hostname, options);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
['resolve','resolve4','resolve6'].forEach(function(method) {
|
|
87
|
+
var orig = dns.promises[method];
|
|
88
|
+
if (!orig) return;
|
|
89
|
+
dns.promises[method] = function(hostname) {
|
|
90
|
+
if (isDomainBlocked(hostname)) return Promise.reject(makeBlockedError(hostname, 'query'));
|
|
91
|
+
return orig.apply(dns.promises, arguments);
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── 1d. network layer safety net: intercept net.connect to telemetry domains ──
|
|
97
|
+
var _origNetConnect = net.connect;
|
|
98
|
+
var _origNetCreateConn = net.createConnection;
|
|
99
|
+
|
|
100
|
+
function getHostFromArgs(args) {
|
|
101
|
+
if (typeof args[0] === 'object') return args[0].host || args[0].hostname || '';
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function makeBlockedSocket(host) {
|
|
106
|
+
var sock = new net.Socket();
|
|
107
|
+
var err = makeBlockedError(host, 'connect');
|
|
108
|
+
process.nextTick(function() { sock.destroy(err); });
|
|
109
|
+
return sock;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
net.connect = function cacNetConnect() {
|
|
113
|
+
var host = getHostFromArgs(arguments);
|
|
114
|
+
if (isDomainBlocked(host)) return makeBlockedSocket(host);
|
|
115
|
+
return _origNetConnect.apply(net, arguments);
|
|
116
|
+
};
|
|
117
|
+
net.createConnection = function cacNetCreateConnection() {
|
|
118
|
+
var host = getHostFromArgs(arguments);
|
|
119
|
+
if (isDomainBlocked(host)) return makeBlockedSocket(host);
|
|
120
|
+
return _origNetCreateConn.apply(net, arguments);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// ─── 2. mTLS certificate injection ──────────────────────────────────
|
|
125
|
+
|
|
126
|
+
var mtlsCert = process.env.CAC_MTLS_CERT;
|
|
127
|
+
var mtlsKey = process.env.CAC_MTLS_KEY;
|
|
128
|
+
var mtlsCa = process.env.CAC_MTLS_CA;
|
|
129
|
+
var proxyHostPort = process.env.CAC_PROXY_HOST || '';
|
|
130
|
+
|
|
131
|
+
if (mtlsCert && mtlsKey) {
|
|
132
|
+
var certData, keyData, caData;
|
|
133
|
+
try {
|
|
134
|
+
certData = fs.readFileSync(mtlsCert);
|
|
135
|
+
keyData = fs.readFileSync(mtlsKey);
|
|
136
|
+
if (mtlsCa) caData = fs.readFileSync(mtlsCa);
|
|
137
|
+
} catch(e) {
|
|
138
|
+
certData = null; keyData = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (certData && keyData) {
|
|
142
|
+
// inject mTLS cert only for proxy connections
|
|
143
|
+
var proxyHost = proxyHostPort.split(':')[0];
|
|
144
|
+
var proxyPort = parseInt(proxyHostPort.split(':')[1], 10) || 0;
|
|
145
|
+
|
|
146
|
+
// 2a. intercept tls.connect, inject client cert for proxy connections
|
|
147
|
+
var _origTlsConnect = tls.connect;
|
|
148
|
+
tls.connect = function cacTlsConnect() {
|
|
149
|
+
// normalize parameters: tls.connect(options[, cb]) or tls.connect(port[, host][, options][, cb])
|
|
150
|
+
var args = Array.prototype.slice.call(arguments);
|
|
151
|
+
var options, callback;
|
|
152
|
+
|
|
153
|
+
if (typeof args[0] === 'object') {
|
|
154
|
+
options = args[0];
|
|
155
|
+
callback = (typeof args[1] === 'function') ? args[1] : undefined;
|
|
156
|
+
} else {
|
|
157
|
+
// tls.connect(port, host, options, cb) form
|
|
158
|
+
var port = args[0];
|
|
159
|
+
var host = (typeof args[1] === 'string') ? args[1] : 'localhost';
|
|
160
|
+
var optIdx = (typeof args[1] === 'string') ? 2 : 1;
|
|
161
|
+
options = (typeof args[optIdx] === 'object') ? args[optIdx] : {};
|
|
162
|
+
options.port = port;
|
|
163
|
+
options.host = host;
|
|
164
|
+
callback = args[args.length - 1];
|
|
165
|
+
if (typeof callback !== 'function') callback = undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// inject only for proxy connections (exact host:port match)
|
|
169
|
+
var targetHost = options.host || options.hostname || '';
|
|
170
|
+
var targetPort = options.port || 0;
|
|
171
|
+
if (proxyHost && targetHost === proxyHost &&
|
|
172
|
+
(proxyPort === 0 || targetPort === proxyPort)) {
|
|
173
|
+
if (!options.cert) {
|
|
174
|
+
options.cert = certData;
|
|
175
|
+
options.key = keyData;
|
|
176
|
+
if (caData) {
|
|
177
|
+
options.ca = options.ca
|
|
178
|
+
? [].concat(options.ca, caData)
|
|
179
|
+
: [caData];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (callback) return _origTlsConnect.call(tls, options, callback);
|
|
185
|
+
return _origTlsConnect.call(tls, options);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// 2b. inject CA into https.globalAgent (trust CA only, no client private key)
|
|
189
|
+
if (caData && https.globalAgent && https.globalAgent.options) {
|
|
190
|
+
https.globalAgent.options.ca = https.globalAgent.options.ca
|
|
191
|
+
? [].concat(https.globalAgent.options.ca, caData)
|
|
192
|
+
: [caData];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// ─── 3. fetch telemetry interception patch ──────────────────────────────────
|
|
199
|
+
// Wrap native fetch to block telemetry domains by URL check
|
|
200
|
+
// (do NOT replace with node-fetch — its Response.body lacks ReadableStream.cancel(),
|
|
201
|
+
// which breaks Bun-based Claude Code streaming)
|
|
202
|
+
|
|
203
|
+
(function patchFetch() {
|
|
204
|
+
if (typeof globalThis === 'undefined' || typeof globalThis.fetch !== 'function') return;
|
|
205
|
+
|
|
206
|
+
var _origFetch = globalThis.fetch;
|
|
207
|
+
globalThis.fetch = function cacFetch(input, init) {
|
|
208
|
+
var hostname;
|
|
209
|
+
try {
|
|
210
|
+
var urlStr = typeof input === 'string' ? input :
|
|
211
|
+
(input && input.url) ? input.url : '';
|
|
212
|
+
if (urlStr) hostname = new URL(urlStr).hostname;
|
|
213
|
+
} catch(e) { /* ignore */ }
|
|
214
|
+
|
|
215
|
+
if (hostname && isDomainBlocked(hostname)) {
|
|
216
|
+
return Promise.reject(makeBlockedError(hostname, 'fetch'));
|
|
217
|
+
}
|
|
218
|
+
return _origFetch(input, init);
|
|
219
|
+
};
|
|
220
|
+
})();
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
// ─── 4. health check bypass (in-process interception, URL-specific) ────────────────
|
|
224
|
+
// Claude Code pings https://api.anthropic.com/api/hello on startup
|
|
225
|
+
// Cloudflare blocks Node.js TLS fingerprint (JA3/JA4) -> 403
|
|
226
|
+
// Solution: intercept this request at Node.js layer, return 200 directly, no network traffic
|
|
227
|
+
// Only intercepts health check, does not affect OAuth/API or other requests
|
|
228
|
+
|
|
229
|
+
function isHealthCheck(url) {
|
|
230
|
+
if (!url) return false;
|
|
231
|
+
// match https://api.anthropic.com/api/hello or variants with query params
|
|
232
|
+
return /^https?:\/\/api\.anthropic\.com\/api\/hello/.test(url);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function makeHealthResponse(callback) {
|
|
236
|
+
var EventEmitter = require('events');
|
|
237
|
+
var body = '{"message":"hello"}';
|
|
238
|
+
|
|
239
|
+
// Fake IncomingMessage
|
|
240
|
+
var res = new EventEmitter();
|
|
241
|
+
res.statusCode = 200;
|
|
242
|
+
res.headers = { 'content-type': 'application/json' };
|
|
243
|
+
res.setEncoding = function() { return res; };
|
|
244
|
+
|
|
245
|
+
// Callback first (so caller attaches handlers), then emit data/end
|
|
246
|
+
if (typeof callback === 'function') {
|
|
247
|
+
process.nextTick(function() {
|
|
248
|
+
callback(res);
|
|
249
|
+
process.nextTick(function() {
|
|
250
|
+
res.emit('data', body);
|
|
251
|
+
res.emit('end');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
process.nextTick(function() {
|
|
256
|
+
res.emit('data', body);
|
|
257
|
+
res.emit('end');
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Fake ClientRequest (no-op)
|
|
262
|
+
var req = new EventEmitter();
|
|
263
|
+
req.end = function() { return req; };
|
|
264
|
+
req.write = function() { return true; };
|
|
265
|
+
req.destroy = function() {};
|
|
266
|
+
req.setTimeout = function() { return req; };
|
|
267
|
+
req.on = function(ev, fn) { EventEmitter.prototype.on.call(req, ev, fn); return req; };
|
|
268
|
+
return req;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 4a. intercept https.get / https.request
|
|
272
|
+
var _origHttpsRequest = https.request;
|
|
273
|
+
var _origHttpsGet = https.get;
|
|
274
|
+
|
|
275
|
+
https.request = function cacHttpsRequest(urlOrOpts, optsOrCb, cb) {
|
|
276
|
+
var url = '';
|
|
277
|
+
if (typeof urlOrOpts === 'string') {
|
|
278
|
+
url = urlOrOpts;
|
|
279
|
+
} else if (urlOrOpts && typeof urlOrOpts === 'object' && urlOrOpts.href) {
|
|
280
|
+
url = urlOrOpts.href;
|
|
281
|
+
} else if (urlOrOpts && typeof urlOrOpts === 'object') {
|
|
282
|
+
var proto = urlOrOpts.protocol || 'https:';
|
|
283
|
+
var host = urlOrOpts.hostname || urlOrOpts.host || '';
|
|
284
|
+
var path = urlOrOpts.path || '/';
|
|
285
|
+
url = proto + '//' + host + path;
|
|
286
|
+
}
|
|
287
|
+
var callback = typeof optsOrCb === 'function' ? optsOrCb : cb;
|
|
288
|
+
if (isHealthCheck(url)) return makeHealthResponse(callback);
|
|
289
|
+
return _origHttpsRequest.apply(https, arguments);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
https.get = function cacHttpsGet(urlOrOpts, optsOrCb, cb) {
|
|
293
|
+
var url = '';
|
|
294
|
+
if (typeof urlOrOpts === 'string') {
|
|
295
|
+
url = urlOrOpts;
|
|
296
|
+
} else if (urlOrOpts && typeof urlOrOpts === 'object' && urlOrOpts.href) {
|
|
297
|
+
url = urlOrOpts.href;
|
|
298
|
+
} else if (urlOrOpts && typeof urlOrOpts === 'object') {
|
|
299
|
+
var proto = urlOrOpts.protocol || 'https:';
|
|
300
|
+
var host = urlOrOpts.hostname || urlOrOpts.host || '';
|
|
301
|
+
var path = urlOrOpts.path || '/';
|
|
302
|
+
url = proto + '//' + host + path;
|
|
303
|
+
}
|
|
304
|
+
var callback = typeof optsOrCb === 'function' ? optsOrCb : cb;
|
|
305
|
+
if (isHealthCheck(url)) return makeHealthResponse(callback);
|
|
306
|
+
return _origHttpsGet.apply(https, arguments);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// 4b. intercept fetch (undici bypasses https module)
|
|
310
|
+
(function patchHealthFetch() {
|
|
311
|
+
if (typeof globalThis === 'undefined' || typeof globalThis.fetch !== 'function') return;
|
|
312
|
+
var _prevFetch = globalThis.fetch;
|
|
313
|
+
globalThis.fetch = function cacHealthFetch(input, init) {
|
|
314
|
+
var url = '';
|
|
315
|
+
try {
|
|
316
|
+
url = typeof input === 'string' ? input : (input && input.url) ? input.url : '';
|
|
317
|
+
} catch(e) {}
|
|
318
|
+
if (isHealthCheck(url)) {
|
|
319
|
+
return Promise.resolve(new Response('{"message":"hello"}', {
|
|
320
|
+
status: 200,
|
|
321
|
+
headers: { 'content-type': 'application/json' }
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
return _prevFetch(input, init);
|
|
325
|
+
};
|
|
326
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-cac",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Isolate, protect, and manage your Claude Code — versions, environments, identity, and proxy.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cac": "cac"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"cac",
|
|
10
10
|
"cac.ps1",
|
|
11
11
|
"cac.cmd",
|
|
12
|
+
"cac-dns-guard.js",
|
|
12
13
|
"fingerprint-hook.js",
|
|
13
14
|
"relay.js",
|
|
14
15
|
"scripts/postinstall.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var fs = require('fs');
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
var pkgDir = path.join(__dirname, '..');
|
|
6
|
+
var cacBin = path.join(pkgDir, 'cac');
|
|
7
|
+
var home = process.env.HOME || process.env.USERPROFILE || '';
|
|
8
|
+
var cacDir = path.join(home, '.cac');
|
|
7
9
|
|
|
8
|
-
//
|
|
10
|
+
// Ensure cac is executable
|
|
11
|
+
try { fs.chmodSync(cacBin, 0o755); } catch (e) {}
|
|
12
|
+
|
|
13
|
+
// Auto-sync runtime files on install/upgrade
|
|
14
|
+
// Pure Node.js — no bash/zsh dependency
|
|
15
|
+
// Ensures bug fixes (dns-guard, relay, fingerprint-hook) take effect immediately
|
|
9
16
|
try {
|
|
10
|
-
fs.
|
|
17
|
+
fs.mkdirSync(cacDir, { recursive: true });
|
|
18
|
+
var files = ['cac-dns-guard.js', 'relay.js', 'fingerprint-hook.js'];
|
|
19
|
+
for (var i = 0; i < files.length; i++) {
|
|
20
|
+
var src = path.join(pkgDir, files[i]);
|
|
21
|
+
var dst = path.join(cacDir, files[i]);
|
|
22
|
+
if (fs.existsSync(src)) {
|
|
23
|
+
fs.copyFileSync(src, dst);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
} catch (e) {
|
|
12
|
-
//
|
|
27
|
+
// Non-fatal — _ensure_initialized will catch it on first cac command
|
|
13
28
|
}
|
|
14
29
|
|
|
15
|
-
console.log(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
cac
|
|
21
|
-
cac
|
|
22
|
-
claude
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
更多信息:https://github.com/nmhjklnm/cac
|
|
29
|
-
`);
|
|
30
|
+
console.log([
|
|
31
|
+
'',
|
|
32
|
+
' claude-cac installed successfully',
|
|
33
|
+
'',
|
|
34
|
+
' Quick start:',
|
|
35
|
+
' cac env create <name> [-p <proxy>] Create an isolated environment',
|
|
36
|
+
' cac <name> Switch environment',
|
|
37
|
+
' claude Start Claude Code',
|
|
38
|
+
'',
|
|
39
|
+
' Docs: https://cac.nextmind.space/docs',
|
|
40
|
+
''
|
|
41
|
+
].join('\n'));
|