@clawos-dev/clawd 0.1.2 → 0.2.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.
- package/README.md +58 -10
- package/dist/cli.cjs +892 -123
- package/dist/cli.cjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -293,8 +293,8 @@ var require_req = __commonJS({
|
|
|
293
293
|
if (req.originalUrl) {
|
|
294
294
|
_req.url = req.originalUrl;
|
|
295
295
|
} else {
|
|
296
|
-
const
|
|
297
|
-
_req.url = typeof
|
|
296
|
+
const path16 = req.path;
|
|
297
|
+
_req.url = typeof path16 === "string" ? path16 : req.url ? req.url.path || req.url : void 0;
|
|
298
298
|
}
|
|
299
299
|
if (req.query) {
|
|
300
300
|
_req.query = req.query;
|
|
@@ -459,14 +459,14 @@ var require_redact = __commonJS({
|
|
|
459
459
|
}
|
|
460
460
|
return obj;
|
|
461
461
|
}
|
|
462
|
-
function parsePath(
|
|
462
|
+
function parsePath(path16) {
|
|
463
463
|
const parts = [];
|
|
464
464
|
let current = "";
|
|
465
465
|
let inBrackets = false;
|
|
466
466
|
let inQuotes = false;
|
|
467
467
|
let quoteChar = "";
|
|
468
|
-
for (let i = 0; i <
|
|
469
|
-
const char =
|
|
468
|
+
for (let i = 0; i < path16.length; i++) {
|
|
469
|
+
const char = path16[i];
|
|
470
470
|
if (!inBrackets && char === ".") {
|
|
471
471
|
if (current) {
|
|
472
472
|
parts.push(current);
|
|
@@ -597,10 +597,10 @@ var require_redact = __commonJS({
|
|
|
597
597
|
return current;
|
|
598
598
|
}
|
|
599
599
|
function redactPaths(obj, paths, censor, remove = false) {
|
|
600
|
-
for (const
|
|
601
|
-
const parts = parsePath(
|
|
600
|
+
for (const path16 of paths) {
|
|
601
|
+
const parts = parsePath(path16);
|
|
602
602
|
if (parts.includes("*")) {
|
|
603
|
-
redactWildcardPath(obj, parts, censor,
|
|
603
|
+
redactWildcardPath(obj, parts, censor, path16, remove);
|
|
604
604
|
} else {
|
|
605
605
|
if (remove) {
|
|
606
606
|
removeKey(obj, parts);
|
|
@@ -685,8 +685,8 @@ var require_redact = __commonJS({
|
|
|
685
685
|
}
|
|
686
686
|
} else {
|
|
687
687
|
if (afterWildcard.includes("*")) {
|
|
688
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
689
|
-
const fullPath = [...pathArray.slice(0, pathLength), ...
|
|
688
|
+
const wrappedCensor = typeof censor === "function" ? (value, path16) => {
|
|
689
|
+
const fullPath = [...pathArray.slice(0, pathLength), ...path16];
|
|
690
690
|
return censor(value, fullPath);
|
|
691
691
|
} : censor;
|
|
692
692
|
redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
|
|
@@ -721,8 +721,8 @@ var require_redact = __commonJS({
|
|
|
721
721
|
return null;
|
|
722
722
|
}
|
|
723
723
|
const pathStructure = /* @__PURE__ */ new Map();
|
|
724
|
-
for (const
|
|
725
|
-
const parts = parsePath(
|
|
724
|
+
for (const path16 of pathsToClone) {
|
|
725
|
+
const parts = parsePath(path16);
|
|
726
726
|
let current = pathStructure;
|
|
727
727
|
for (let i = 0; i < parts.length; i++) {
|
|
728
728
|
const part = parts[i];
|
|
@@ -774,24 +774,24 @@ var require_redact = __commonJS({
|
|
|
774
774
|
}
|
|
775
775
|
return cloneSelectively(obj, pathStructure);
|
|
776
776
|
}
|
|
777
|
-
function validatePath(
|
|
778
|
-
if (typeof
|
|
777
|
+
function validatePath(path16) {
|
|
778
|
+
if (typeof path16 !== "string") {
|
|
779
779
|
throw new Error("Paths must be (non-empty) strings");
|
|
780
780
|
}
|
|
781
|
-
if (
|
|
781
|
+
if (path16 === "") {
|
|
782
782
|
throw new Error("Invalid redaction path ()");
|
|
783
783
|
}
|
|
784
|
-
if (
|
|
785
|
-
throw new Error(`Invalid redaction path (${
|
|
784
|
+
if (path16.includes("..")) {
|
|
785
|
+
throw new Error(`Invalid redaction path (${path16})`);
|
|
786
786
|
}
|
|
787
|
-
if (
|
|
788
|
-
throw new Error(`Invalid redaction path (${
|
|
787
|
+
if (path16.includes(",")) {
|
|
788
|
+
throw new Error(`Invalid redaction path (${path16})`);
|
|
789
789
|
}
|
|
790
790
|
let bracketCount = 0;
|
|
791
791
|
let inQuotes = false;
|
|
792
792
|
let quoteChar = "";
|
|
793
|
-
for (let i = 0; i <
|
|
794
|
-
const char =
|
|
793
|
+
for (let i = 0; i < path16.length; i++) {
|
|
794
|
+
const char = path16[i];
|
|
795
795
|
if ((char === '"' || char === "'") && bracketCount > 0) {
|
|
796
796
|
if (!inQuotes) {
|
|
797
797
|
inQuotes = true;
|
|
@@ -805,20 +805,20 @@ var require_redact = __commonJS({
|
|
|
805
805
|
} else if (char === "]" && !inQuotes) {
|
|
806
806
|
bracketCount--;
|
|
807
807
|
if (bracketCount < 0) {
|
|
808
|
-
throw new Error(`Invalid redaction path (${
|
|
808
|
+
throw new Error(`Invalid redaction path (${path16})`);
|
|
809
809
|
}
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
812
|
if (bracketCount !== 0) {
|
|
813
|
-
throw new Error(`Invalid redaction path (${
|
|
813
|
+
throw new Error(`Invalid redaction path (${path16})`);
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
function validatePaths(paths) {
|
|
817
817
|
if (!Array.isArray(paths)) {
|
|
818
818
|
throw new TypeError("paths must be an array");
|
|
819
819
|
}
|
|
820
|
-
for (const
|
|
821
|
-
validatePath(
|
|
820
|
+
for (const path16 of paths) {
|
|
821
|
+
validatePath(path16);
|
|
822
822
|
}
|
|
823
823
|
}
|
|
824
824
|
function slowRedact(options = {}) {
|
|
@@ -986,8 +986,8 @@ var require_redaction = __commonJS({
|
|
|
986
986
|
if (shape[k] === null) {
|
|
987
987
|
o[k] = (value) => topCensor(value, [k]);
|
|
988
988
|
} else {
|
|
989
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
990
|
-
return censor(value, [k, ...
|
|
989
|
+
const wrappedCensor = typeof censor === "function" ? (value, path16) => {
|
|
990
|
+
return censor(value, [k, ...path16]);
|
|
991
991
|
} : censor;
|
|
992
992
|
o[k] = Redact({
|
|
993
993
|
paths: shape[k],
|
|
@@ -1205,10 +1205,10 @@ var require_atomic_sleep = __commonJS({
|
|
|
1205
1205
|
var require_sonic_boom = __commonJS({
|
|
1206
1206
|
"../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
|
|
1207
1207
|
"use strict";
|
|
1208
|
-
var
|
|
1208
|
+
var fs16 = require("fs");
|
|
1209
1209
|
var EventEmitter = require("events");
|
|
1210
1210
|
var inherits = require("util").inherits;
|
|
1211
|
-
var
|
|
1211
|
+
var path16 = require("path");
|
|
1212
1212
|
var sleep = require_atomic_sleep();
|
|
1213
1213
|
var assert = require("assert");
|
|
1214
1214
|
var BUSY_WRITE_TIMEOUT = 100;
|
|
@@ -1262,20 +1262,20 @@ var require_sonic_boom = __commonJS({
|
|
|
1262
1262
|
const mode = sonic.mode;
|
|
1263
1263
|
if (sonic.sync) {
|
|
1264
1264
|
try {
|
|
1265
|
-
if (sonic.mkdir)
|
|
1266
|
-
const fd =
|
|
1265
|
+
if (sonic.mkdir) fs16.mkdirSync(path16.dirname(file), { recursive: true });
|
|
1266
|
+
const fd = fs16.openSync(file, flags, mode);
|
|
1267
1267
|
fileOpened(null, fd);
|
|
1268
1268
|
} catch (err) {
|
|
1269
1269
|
fileOpened(err);
|
|
1270
1270
|
throw err;
|
|
1271
1271
|
}
|
|
1272
1272
|
} else if (sonic.mkdir) {
|
|
1273
|
-
|
|
1273
|
+
fs16.mkdir(path16.dirname(file), { recursive: true }, (err) => {
|
|
1274
1274
|
if (err) return fileOpened(err);
|
|
1275
|
-
|
|
1275
|
+
fs16.open(file, flags, mode, fileOpened);
|
|
1276
1276
|
});
|
|
1277
1277
|
} else {
|
|
1278
|
-
|
|
1278
|
+
fs16.open(file, flags, mode, fileOpened);
|
|
1279
1279
|
}
|
|
1280
1280
|
}
|
|
1281
1281
|
function SonicBoom(opts) {
|
|
@@ -1316,8 +1316,8 @@ var require_sonic_boom = __commonJS({
|
|
|
1316
1316
|
this.flush = flushBuffer;
|
|
1317
1317
|
this.flushSync = flushBufferSync;
|
|
1318
1318
|
this._actualWrite = actualWriteBuffer;
|
|
1319
|
-
fsWriteSync = () =>
|
|
1320
|
-
fsWrite = () =>
|
|
1319
|
+
fsWriteSync = () => fs16.writeSync(this.fd, this._writingBuf);
|
|
1320
|
+
fsWrite = () => fs16.write(this.fd, this._writingBuf, this.release);
|
|
1321
1321
|
} else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
|
|
1322
1322
|
this._writingBuf = "";
|
|
1323
1323
|
this.write = write;
|
|
@@ -1326,15 +1326,15 @@ var require_sonic_boom = __commonJS({
|
|
|
1326
1326
|
this._actualWrite = actualWrite;
|
|
1327
1327
|
fsWriteSync = () => {
|
|
1328
1328
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
1329
|
-
return
|
|
1329
|
+
return fs16.writeSync(this.fd, this._writingBuf);
|
|
1330
1330
|
}
|
|
1331
|
-
return
|
|
1331
|
+
return fs16.writeSync(this.fd, this._writingBuf, "utf8");
|
|
1332
1332
|
};
|
|
1333
1333
|
fsWrite = () => {
|
|
1334
1334
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
1335
|
-
return
|
|
1335
|
+
return fs16.write(this.fd, this._writingBuf, this.release);
|
|
1336
1336
|
}
|
|
1337
|
-
return
|
|
1337
|
+
return fs16.write(this.fd, this._writingBuf, "utf8", this.release);
|
|
1338
1338
|
};
|
|
1339
1339
|
} else {
|
|
1340
1340
|
throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
|
|
@@ -1391,7 +1391,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1391
1391
|
}
|
|
1392
1392
|
}
|
|
1393
1393
|
if (this._fsync) {
|
|
1394
|
-
|
|
1394
|
+
fs16.fsyncSync(this.fd);
|
|
1395
1395
|
}
|
|
1396
1396
|
const len = this._len;
|
|
1397
1397
|
if (this._reopening) {
|
|
@@ -1505,7 +1505,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1505
1505
|
const onDrain = () => {
|
|
1506
1506
|
if (!this._fsync) {
|
|
1507
1507
|
try {
|
|
1508
|
-
|
|
1508
|
+
fs16.fsync(this.fd, (err) => {
|
|
1509
1509
|
this._flushPending = false;
|
|
1510
1510
|
cb(err);
|
|
1511
1511
|
});
|
|
@@ -1607,7 +1607,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1607
1607
|
const fd = this.fd;
|
|
1608
1608
|
this.once("ready", () => {
|
|
1609
1609
|
if (fd !== this.fd) {
|
|
1610
|
-
|
|
1610
|
+
fs16.close(fd, (err) => {
|
|
1611
1611
|
if (err) {
|
|
1612
1612
|
return this.emit("error", err);
|
|
1613
1613
|
}
|
|
@@ -1656,7 +1656,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1656
1656
|
buf = this._bufs[0];
|
|
1657
1657
|
}
|
|
1658
1658
|
try {
|
|
1659
|
-
const n = Buffer.isBuffer(buf) ?
|
|
1659
|
+
const n = Buffer.isBuffer(buf) ? fs16.writeSync(this.fd, buf) : fs16.writeSync(this.fd, buf, "utf8");
|
|
1660
1660
|
const releasedBufObj = releaseWritingBuf(buf, this._len, n);
|
|
1661
1661
|
buf = releasedBufObj.writingBuf;
|
|
1662
1662
|
this._len = releasedBufObj.len;
|
|
@@ -1672,7 +1672,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1672
1672
|
}
|
|
1673
1673
|
}
|
|
1674
1674
|
try {
|
|
1675
|
-
|
|
1675
|
+
fs16.fsyncSync(this.fd);
|
|
1676
1676
|
} catch {
|
|
1677
1677
|
}
|
|
1678
1678
|
}
|
|
@@ -1693,7 +1693,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1693
1693
|
buf = mergeBuf(this._bufs[0], this._lens[0]);
|
|
1694
1694
|
}
|
|
1695
1695
|
try {
|
|
1696
|
-
const n =
|
|
1696
|
+
const n = fs16.writeSync(this.fd, buf);
|
|
1697
1697
|
buf = buf.subarray(n);
|
|
1698
1698
|
this._len = Math.max(this._len - n, 0);
|
|
1699
1699
|
if (buf.length <= 0) {
|
|
@@ -1721,13 +1721,13 @@ var require_sonic_boom = __commonJS({
|
|
|
1721
1721
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
|
|
1722
1722
|
if (this.sync) {
|
|
1723
1723
|
try {
|
|
1724
|
-
const written = Buffer.isBuffer(this._writingBuf) ?
|
|
1724
|
+
const written = Buffer.isBuffer(this._writingBuf) ? fs16.writeSync(this.fd, this._writingBuf) : fs16.writeSync(this.fd, this._writingBuf, "utf8");
|
|
1725
1725
|
release(null, written);
|
|
1726
1726
|
} catch (err) {
|
|
1727
1727
|
release(err);
|
|
1728
1728
|
}
|
|
1729
1729
|
} else {
|
|
1730
|
-
|
|
1730
|
+
fs16.write(this.fd, this._writingBuf, release);
|
|
1731
1731
|
}
|
|
1732
1732
|
}
|
|
1733
1733
|
function actualWriteBuffer() {
|
|
@@ -1736,7 +1736,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1736
1736
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
|
|
1737
1737
|
if (this.sync) {
|
|
1738
1738
|
try {
|
|
1739
|
-
const written =
|
|
1739
|
+
const written = fs16.writeSync(this.fd, this._writingBuf);
|
|
1740
1740
|
release(null, written);
|
|
1741
1741
|
} catch (err) {
|
|
1742
1742
|
release(err);
|
|
@@ -1745,7 +1745,7 @@ var require_sonic_boom = __commonJS({
|
|
|
1745
1745
|
if (kCopyBuffer) {
|
|
1746
1746
|
this._writingBuf = Buffer.from(this._writingBuf);
|
|
1747
1747
|
}
|
|
1748
|
-
|
|
1748
|
+
fs16.write(this.fd, this._writingBuf, release);
|
|
1749
1749
|
}
|
|
1750
1750
|
}
|
|
1751
1751
|
function actualClose(sonic) {
|
|
@@ -1761,12 +1761,12 @@ var require_sonic_boom = __commonJS({
|
|
|
1761
1761
|
sonic._lens = [];
|
|
1762
1762
|
assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
|
|
1763
1763
|
try {
|
|
1764
|
-
|
|
1764
|
+
fs16.fsync(sonic.fd, closeWrapped);
|
|
1765
1765
|
} catch {
|
|
1766
1766
|
}
|
|
1767
1767
|
function closeWrapped() {
|
|
1768
1768
|
if (sonic.fd !== 1 && sonic.fd !== 2) {
|
|
1769
|
-
|
|
1769
|
+
fs16.close(sonic.fd, done);
|
|
1770
1770
|
} else {
|
|
1771
1771
|
done();
|
|
1772
1772
|
}
|
|
@@ -2497,7 +2497,7 @@ var require_transport = __commonJS({
|
|
|
2497
2497
|
stream.flushSync();
|
|
2498
2498
|
}
|
|
2499
2499
|
function transport(fullOptions) {
|
|
2500
|
-
const { pipeline, targets, levels, dedupe, worker = {}, caller = getCallers(), sync = false } = fullOptions;
|
|
2500
|
+
const { pipeline: pipeline2, targets, levels, dedupe, worker = {}, caller = getCallers(), sync = false } = fullOptions;
|
|
2501
2501
|
const options = {
|
|
2502
2502
|
...fullOptions.options
|
|
2503
2503
|
};
|
|
@@ -2525,9 +2525,9 @@ var require_transport = __commonJS({
|
|
|
2525
2525
|
};
|
|
2526
2526
|
});
|
|
2527
2527
|
});
|
|
2528
|
-
} else if (
|
|
2528
|
+
} else if (pipeline2) {
|
|
2529
2529
|
target = bundlerOverrides["pino-worker"] || join2(__dirname, "worker.js");
|
|
2530
|
-
options.pipelines = [
|
|
2530
|
+
options.pipelines = [pipeline2.map((dest) => {
|
|
2531
2531
|
return {
|
|
2532
2532
|
...dest,
|
|
2533
2533
|
target: fixTarget(dest.target)
|
|
@@ -4130,7 +4130,7 @@ var require_multistream = __commonJS({
|
|
|
4130
4130
|
var require_pino = __commonJS({
|
|
4131
4131
|
"../node_modules/.pnpm/pino@9.14.0/node_modules/pino/pino.js"(exports2, module2) {
|
|
4132
4132
|
"use strict";
|
|
4133
|
-
var
|
|
4133
|
+
var os11 = require("os");
|
|
4134
4134
|
var stdSerializers = require_pino_std_serializers();
|
|
4135
4135
|
var caller = require_caller();
|
|
4136
4136
|
var redaction = require_redaction();
|
|
@@ -4177,7 +4177,7 @@ var require_pino = __commonJS({
|
|
|
4177
4177
|
} = symbols;
|
|
4178
4178
|
var { epochTime, nullTime } = time;
|
|
4179
4179
|
var { pid } = process;
|
|
4180
|
-
var hostname =
|
|
4180
|
+
var hostname = os11.hostname();
|
|
4181
4181
|
var defaultErrorSerializer = stdSerializers.err;
|
|
4182
4182
|
var defaultOptions = {
|
|
4183
4183
|
level: "info",
|
|
@@ -6540,7 +6540,7 @@ var require_websocket = __commonJS({
|
|
|
6540
6540
|
var net = require("net");
|
|
6541
6541
|
var tls = require("tls");
|
|
6542
6542
|
var { randomBytes, createHash } = require("crypto");
|
|
6543
|
-
var { Duplex, Readable } = require("stream");
|
|
6543
|
+
var { Duplex, Readable: Readable2 } = require("stream");
|
|
6544
6544
|
var { URL: URL2 } = require("url");
|
|
6545
6545
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
6546
6546
|
var Receiver2 = require_receiver();
|
|
@@ -7958,6 +7958,7 @@ var import_node_os = __toESM(require("os"), 1);
|
|
|
7958
7958
|
var import_node_path = __toESM(require("path"), 1);
|
|
7959
7959
|
var DEFAULT_PORT = 18790;
|
|
7960
7960
|
var DEFAULT_HOST = "127.0.0.1";
|
|
7961
|
+
var DEFAULT_CLAWOS_API = "https://api.clawos.chat";
|
|
7961
7962
|
function parseArgs(argv) {
|
|
7962
7963
|
const out = {};
|
|
7963
7964
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -7986,8 +7987,14 @@ function parseArgs(argv) {
|
|
|
7986
7987
|
out.logLevel = v;
|
|
7987
7988
|
break;
|
|
7988
7989
|
}
|
|
7989
|
-
case "--
|
|
7990
|
-
out.
|
|
7990
|
+
case "--tunnel":
|
|
7991
|
+
out.tunnel = true;
|
|
7992
|
+
break;
|
|
7993
|
+
case "--clawos-api":
|
|
7994
|
+
out.clawosApi = next();
|
|
7995
|
+
break;
|
|
7996
|
+
case "--auth-token":
|
|
7997
|
+
out.authToken = next();
|
|
7991
7998
|
break;
|
|
7992
7999
|
case "--help":
|
|
7993
8000
|
case "-h":
|
|
@@ -8029,14 +8036,26 @@ function resolveConfig(opts) {
|
|
|
8029
8036
|
const port = args.port ?? (typeof fileCfg.port === "number" ? fileCfg.port : DEFAULT_PORT);
|
|
8030
8037
|
const host = typeof fileCfg.host === "string" && fileCfg.host || DEFAULT_HOST;
|
|
8031
8038
|
const logLevel = args.logLevel ?? fileCfg.logLevel ?? "info";
|
|
8032
|
-
const
|
|
8039
|
+
const env = opts.env;
|
|
8040
|
+
const fileTunnel = fileCfg.tunnel === true;
|
|
8041
|
+
const fileClawosApi = typeof fileCfg.clawosApi === "string" ? fileCfg.clawosApi : null;
|
|
8042
|
+
const fileAuthToken = typeof fileCfg.authToken === "string" ? fileCfg.authToken : null;
|
|
8043
|
+
const tunnel = args.tunnel === true || fileTunnel || env.CLAWD_TUNNEL === "1";
|
|
8044
|
+
const clawosApi = args.clawosApi ?? env.CLAWOS_API ?? fileClawosApi ?? (tunnel ? DEFAULT_CLAWOS_API : null);
|
|
8045
|
+
const authToken = args.authToken ?? env.CLAWD_AUTH_TOKEN ?? fileAuthToken;
|
|
8046
|
+
const noTunnelPersist = env.CLAWD_NO_TUNNEL_PERSIST === "1" || env.NODE_ENV === "development";
|
|
8047
|
+
const frpcBinary = env.CLAWD_FRPC_BIN ?? null;
|
|
8033
8048
|
return {
|
|
8034
8049
|
port,
|
|
8035
8050
|
host,
|
|
8036
8051
|
dataDir,
|
|
8037
8052
|
configPath: args.config ? configPath : import_node_fs.default.existsSync(configPath) ? configPath : null,
|
|
8038
8053
|
logLevel,
|
|
8039
|
-
|
|
8054
|
+
tunnel,
|
|
8055
|
+
clawosApi,
|
|
8056
|
+
authToken,
|
|
8057
|
+
noTunnelPersist,
|
|
8058
|
+
frpcBinary
|
|
8040
8059
|
};
|
|
8041
8060
|
}
|
|
8042
8061
|
var HELP_TEXT = `clawd [options]
|
|
@@ -8044,14 +8063,20 @@ var HELP_TEXT = `clawd [options]
|
|
|
8044
8063
|
--data-dir <path> \u6570\u636E\u76EE\u5F55\uFF0C\u9ED8\u8BA4 ~/.clawd
|
|
8045
8064
|
--config <path> \u914D\u7F6E\u6587\u4EF6\uFF0C\u9ED8\u8BA4 <data-dir>/config.json
|
|
8046
8065
|
--log-level <level> debug / info / warn / error\uFF08\u9ED8\u8BA4 info\uFF09
|
|
8047
|
-
--
|
|
8066
|
+
--tunnel \u542F\u7528 frp tunnel \u628A\u672C\u5730\u7AEF\u53E3\u66B4\u9732\u5230\u516C\u7F51
|
|
8067
|
+
--clawos-api <url> tunnel register \u63A5\u53E3\u7684 base url\uFF08\u9ED8\u8BA4 https://api.clawos.chat\uFF09
|
|
8068
|
+
--auth-token <s> \u6307\u5B9A daemon auth token\uFF1B\u7F3A\u7701\u65F6 tunnel \u6A21\u5F0F\u4ECE ~/.clawd/auth.json \u590D\u7528\uFF0C\u6CA1\u6709\u5C31\u751F\u6210
|
|
8048
8069
|
--help / -h \u663E\u793A\u5E2E\u52A9
|
|
8049
8070
|
--version / -v \u663E\u793A\u7248\u672C
|
|
8071
|
+
|
|
8072
|
+
Env (advanced):
|
|
8073
|
+
CLAWD_FRPC_BIN \u81EA\u5E26 frpc \u4E8C\u8FDB\u5236\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u6309\u9700\u4E0B\u8F7D\u5230 ~/.clawd/bin/frpc\uFF09
|
|
8074
|
+
CLAWD_NO_TUNNEL_PERSIST \u542F\u52A8\u65F6\u4E0D\u590D\u7528 ~/.clawd/tunnel.json\uFF08dev only\uFF1BNODE_ENV=development \u81EA\u52A8\u5F00\u542F\uFF09
|
|
8050
8075
|
`;
|
|
8051
8076
|
|
|
8052
8077
|
// src/index.ts
|
|
8053
|
-
var
|
|
8054
|
-
var
|
|
8078
|
+
var import_node_os10 = __toESM(require("os"), 1);
|
|
8079
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
8055
8080
|
|
|
8056
8081
|
// src/logger.ts
|
|
8057
8082
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
@@ -8642,8 +8667,8 @@ function getErrorMap() {
|
|
|
8642
8667
|
|
|
8643
8668
|
// ../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
|
|
8644
8669
|
var makeIssue = (params) => {
|
|
8645
|
-
const { data, path:
|
|
8646
|
-
const fullPath = [...
|
|
8670
|
+
const { data, path: path16, errorMaps, issueData } = params;
|
|
8671
|
+
const fullPath = [...path16, ...issueData.path || []];
|
|
8647
8672
|
const fullIssue = {
|
|
8648
8673
|
...issueData,
|
|
8649
8674
|
path: fullPath
|
|
@@ -8759,11 +8784,11 @@ var errorUtil;
|
|
|
8759
8784
|
|
|
8760
8785
|
// ../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
|
|
8761
8786
|
var ParseInputLazyPath = class {
|
|
8762
|
-
constructor(parent, value,
|
|
8787
|
+
constructor(parent, value, path16, key) {
|
|
8763
8788
|
this._cachedPath = [];
|
|
8764
8789
|
this.parent = parent;
|
|
8765
8790
|
this.data = value;
|
|
8766
|
-
this._path =
|
|
8791
|
+
this._path = path16;
|
|
8767
8792
|
this._key = key;
|
|
8768
8793
|
}
|
|
8769
8794
|
get path() {
|
|
@@ -12591,6 +12616,22 @@ var RecentDirEntrySchema = external_exports.object({
|
|
|
12591
12616
|
var HistoryRecentDirsResponseSchema = external_exports.object({
|
|
12592
12617
|
dirs: external_exports.array(RecentDirEntrySchema)
|
|
12593
12618
|
});
|
|
12619
|
+
var AuthRequestFrameSchema = external_exports.object({
|
|
12620
|
+
type: external_exports.literal("auth"),
|
|
12621
|
+
token: external_exports.string().min(1),
|
|
12622
|
+
scheme: external_exports.literal("bearer").optional()
|
|
12623
|
+
});
|
|
12624
|
+
var AuthOkFrameSchema = external_exports.object({
|
|
12625
|
+
type: external_exports.literal("auth:ok"),
|
|
12626
|
+
version: external_exports.string().min(1).optional(),
|
|
12627
|
+
protocolVersion: external_exports.number().int().nonnegative().optional()
|
|
12628
|
+
});
|
|
12629
|
+
var TunnelExitedEventSchema = external_exports.object({
|
|
12630
|
+
type: external_exports.literal("tunnel:exited"),
|
|
12631
|
+
code: external_exports.number().int().nullable(),
|
|
12632
|
+
subdomain: external_exports.string().nullable(),
|
|
12633
|
+
url: external_exports.string().nullable()
|
|
12634
|
+
});
|
|
12594
12635
|
|
|
12595
12636
|
// ../protocol/src/frames.ts
|
|
12596
12637
|
var PROTOCOL_VERSION = 1;
|
|
@@ -14005,11 +14046,11 @@ Diff.prototype = {
|
|
|
14005
14046
|
}
|
|
14006
14047
|
}
|
|
14007
14048
|
},
|
|
14008
|
-
addToPath: function addToPath(
|
|
14009
|
-
var last =
|
|
14049
|
+
addToPath: function addToPath(path16, added, removed, oldPosInc, options) {
|
|
14050
|
+
var last = path16.lastComponent;
|
|
14010
14051
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
14011
14052
|
return {
|
|
14012
|
-
oldPos:
|
|
14053
|
+
oldPos: path16.oldPos + oldPosInc,
|
|
14013
14054
|
lastComponent: {
|
|
14014
14055
|
count: last.count + 1,
|
|
14015
14056
|
added,
|
|
@@ -14019,7 +14060,7 @@ Diff.prototype = {
|
|
|
14019
14060
|
};
|
|
14020
14061
|
} else {
|
|
14021
14062
|
return {
|
|
14022
|
-
oldPos:
|
|
14063
|
+
oldPos: path16.oldPos + oldPosInc,
|
|
14023
14064
|
lastComponent: {
|
|
14024
14065
|
count: 1,
|
|
14025
14066
|
added,
|
|
@@ -14862,10 +14903,10 @@ function attachmentToHistoryMessage(o, ts) {
|
|
|
14862
14903
|
const memories = raw.map((m) => {
|
|
14863
14904
|
if (!m || typeof m !== "object") return null;
|
|
14864
14905
|
const rec = m;
|
|
14865
|
-
const
|
|
14906
|
+
const path16 = typeof rec.path === "string" ? rec.path : null;
|
|
14866
14907
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
14867
|
-
if (!
|
|
14868
|
-
const entry = { path:
|
|
14908
|
+
if (!path16 || content == null) return null;
|
|
14909
|
+
const entry = { path: path16, content };
|
|
14869
14910
|
if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
|
|
14870
14911
|
return entry;
|
|
14871
14912
|
}).filter((m) => m !== null);
|
|
@@ -15601,10 +15642,10 @@ function parseAttachment(obj) {
|
|
|
15601
15642
|
const memories = raw.map((m) => {
|
|
15602
15643
|
if (!m || typeof m !== "object") return null;
|
|
15603
15644
|
const rec = m;
|
|
15604
|
-
const
|
|
15645
|
+
const path16 = typeof rec.path === "string" ? rec.path : null;
|
|
15605
15646
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
15606
|
-
if (!
|
|
15607
|
-
const out = { path:
|
|
15647
|
+
if (!path16 || content == null) return null;
|
|
15648
|
+
const out = { path: path16, content };
|
|
15608
15649
|
if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
|
|
15609
15650
|
return out;
|
|
15610
15651
|
}).filter((m) => m !== null);
|
|
@@ -16217,7 +16258,7 @@ var LocalWsServer = class {
|
|
|
16217
16258
|
this.logger?.error("ws server error", { err: err.message });
|
|
16218
16259
|
reject(err);
|
|
16219
16260
|
});
|
|
16220
|
-
wss.on("connection", (socket) => this.onConnection(socket));
|
|
16261
|
+
wss.on("connection", (socket, req) => this.onConnection(socket, req?.socket?.remoteAddress));
|
|
16221
16262
|
this.wss = wss;
|
|
16222
16263
|
});
|
|
16223
16264
|
}
|
|
@@ -16246,13 +16287,37 @@ var LocalWsServer = class {
|
|
|
16246
16287
|
}
|
|
16247
16288
|
}
|
|
16248
16289
|
}
|
|
16290
|
+
// 广播给所有已连接客户端(无视 session 订阅)。
|
|
16291
|
+
// 用于 daemon 级事件,例如 tunnel:exited。未通过 auth 的连接被自动跳过。
|
|
16292
|
+
broadcastAll(frame) {
|
|
16293
|
+
const gate = this.opts.authGate;
|
|
16294
|
+
for (const c of this.clients.values()) {
|
|
16295
|
+
if (gate && !gate.isAuthed(c.handle.id)) continue;
|
|
16296
|
+
this.safeSend(c.ws, frame);
|
|
16297
|
+
}
|
|
16298
|
+
}
|
|
16249
16299
|
firstSubscriber(sessionId) {
|
|
16250
16300
|
for (const c of this.clients.values()) {
|
|
16251
16301
|
if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
|
|
16252
16302
|
}
|
|
16253
16303
|
return null;
|
|
16254
16304
|
}
|
|
16255
|
-
|
|
16305
|
+
// AuthGate 用:按 client id 关闭连接(替代之前的反射访问)
|
|
16306
|
+
closeClient(clientId, code, reason) {
|
|
16307
|
+
const c = this.clients.get(clientId);
|
|
16308
|
+
if (!c) return;
|
|
16309
|
+
try {
|
|
16310
|
+
c.ws.close(code, reason);
|
|
16311
|
+
} catch {
|
|
16312
|
+
}
|
|
16313
|
+
}
|
|
16314
|
+
// AuthGate 用:按 client id 发送一帧(auth:ok 等)
|
|
16315
|
+
sendToClient(clientId, frame) {
|
|
16316
|
+
const c = this.clients.get(clientId);
|
|
16317
|
+
if (!c) return;
|
|
16318
|
+
this.safeSend(c.ws, frame);
|
|
16319
|
+
}
|
|
16320
|
+
onConnection(socket, remoteAddress) {
|
|
16256
16321
|
const id = v4_default();
|
|
16257
16322
|
const subscribedSessions = /* @__PURE__ */ new Set();
|
|
16258
16323
|
const handle = {
|
|
@@ -16268,27 +16333,38 @@ var LocalWsServer = class {
|
|
|
16268
16333
|
}
|
|
16269
16334
|
}, this.pingIntervalMs);
|
|
16270
16335
|
this.clients.set(id, { handle, ws: socket, pingTimer });
|
|
16271
|
-
this.logger?.info("client connected", { clientId: id, total: this.clients.size });
|
|
16336
|
+
this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
|
|
16337
|
+
const authGate = this.opts.authGate;
|
|
16338
|
+
let authed = true;
|
|
16339
|
+
if (authGate) {
|
|
16340
|
+
const authHandle = { id, remoteAddress };
|
|
16341
|
+
authed = authGate.registerConnection(authHandle);
|
|
16342
|
+
}
|
|
16272
16343
|
try {
|
|
16273
|
-
|
|
16274
|
-
|
|
16344
|
+
if (authed) {
|
|
16345
|
+
const readyFrame = this.opts.readyFrameBuilder();
|
|
16346
|
+
this.safeSend(socket, { type: "ready", ...readyFrame });
|
|
16347
|
+
} else {
|
|
16348
|
+
this.safeSend(socket, { type: "ready", protocolVersion: this.opts.protocolVersion });
|
|
16349
|
+
}
|
|
16275
16350
|
} catch (err) {
|
|
16276
16351
|
this.logger?.warn("ready frame build failed", { err: err.message });
|
|
16277
16352
|
}
|
|
16278
16353
|
socket.on("message", (data) => {
|
|
16279
|
-
this.onMessage(handle, data);
|
|
16354
|
+
this.onMessage(handle, data, remoteAddress);
|
|
16280
16355
|
});
|
|
16281
16356
|
socket.on("close", () => {
|
|
16282
16357
|
const c = this.clients.get(id);
|
|
16283
16358
|
if (c?.pingTimer) clearInterval(c.pingTimer);
|
|
16284
16359
|
this.clients.delete(id);
|
|
16360
|
+
authGate?.unregister(id);
|
|
16285
16361
|
this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
|
|
16286
16362
|
});
|
|
16287
16363
|
socket.on("error", (err) => {
|
|
16288
16364
|
this.logger?.warn("client socket error", { clientId: id, err: err.message });
|
|
16289
16365
|
});
|
|
16290
16366
|
}
|
|
16291
|
-
onMessage(client, data) {
|
|
16367
|
+
onMessage(client, data, remoteAddress) {
|
|
16292
16368
|
let frame;
|
|
16293
16369
|
try {
|
|
16294
16370
|
frame = JSON.parse(data.toString());
|
|
@@ -16296,6 +16372,22 @@ var LocalWsServer = class {
|
|
|
16296
16372
|
this.safeSend(this.clients.get(client.id).ws, { type: "error", code: "VALIDATION_ERROR", message: "invalid json" });
|
|
16297
16373
|
return;
|
|
16298
16374
|
}
|
|
16375
|
+
const authGate = this.opts.authGate;
|
|
16376
|
+
if (authGate) {
|
|
16377
|
+
const wasAuthed = authGate.isAuthed(client.id);
|
|
16378
|
+
const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
|
|
16379
|
+
if (verdict !== "pass") {
|
|
16380
|
+
if (!wasAuthed && authGate.isAuthed(client.id)) {
|
|
16381
|
+
try {
|
|
16382
|
+
const full = this.opts.readyFrameBuilder();
|
|
16383
|
+
this.safeSend(this.clients.get(client.id).ws, { type: "ready", ...full });
|
|
16384
|
+
} catch (err) {
|
|
16385
|
+
this.logger?.warn("post-auth ready frame build failed", { err: err.message });
|
|
16386
|
+
}
|
|
16387
|
+
}
|
|
16388
|
+
return;
|
|
16389
|
+
}
|
|
16390
|
+
}
|
|
16299
16391
|
if (frame.type === "ping") {
|
|
16300
16392
|
this.safeSend(this.clients.get(client.id).ws, { type: "pong", at: Date.now() });
|
|
16301
16393
|
return;
|
|
@@ -16340,6 +16432,99 @@ var LocalWsServer = class {
|
|
|
16340
16432
|
}
|
|
16341
16433
|
};
|
|
16342
16434
|
|
|
16435
|
+
// src/transport/auth.ts
|
|
16436
|
+
var AuthGate = class {
|
|
16437
|
+
states = /* @__PURE__ */ new Map();
|
|
16438
|
+
opts;
|
|
16439
|
+
constructor(opts) {
|
|
16440
|
+
this.opts = { ...opts, timeoutMs: opts.timeoutMs ?? 5e3 };
|
|
16441
|
+
}
|
|
16442
|
+
// 新连接接入;返回 isAuthed(豁免时直接 true)
|
|
16443
|
+
registerConnection(handle) {
|
|
16444
|
+
const enforce = this.opts.shouldEnforce(handle.remoteAddress);
|
|
16445
|
+
if (!enforce) {
|
|
16446
|
+
this.states.set(handle.id, { authed: true, enforce: false, timer: null });
|
|
16447
|
+
return true;
|
|
16448
|
+
}
|
|
16449
|
+
const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
|
|
16450
|
+
const timer = setT(() => {
|
|
16451
|
+
const st = this.states.get(handle.id);
|
|
16452
|
+
if (!st || st.authed) return;
|
|
16453
|
+
this.opts.closeConnection(handle, 1008, "auth timeout");
|
|
16454
|
+
}, this.opts.timeoutMs);
|
|
16455
|
+
this.states.set(handle.id, { authed: false, enforce: true, timer });
|
|
16456
|
+
return false;
|
|
16457
|
+
}
|
|
16458
|
+
unregister(handleId) {
|
|
16459
|
+
const st = this.states.get(handleId);
|
|
16460
|
+
if (st?.timer != null) {
|
|
16461
|
+
const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
16462
|
+
clear(st.timer);
|
|
16463
|
+
}
|
|
16464
|
+
this.states.delete(handleId);
|
|
16465
|
+
}
|
|
16466
|
+
isAuthed(handleId) {
|
|
16467
|
+
return this.states.get(handleId)?.authed ?? false;
|
|
16468
|
+
}
|
|
16469
|
+
// 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
|
|
16470
|
+
// - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
|
|
16471
|
+
// - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
|
|
16472
|
+
// - pass:已 authed,调用方继续走业务路由
|
|
16473
|
+
handleFrame(handle, frame) {
|
|
16474
|
+
const st = this.states.get(handle.id);
|
|
16475
|
+
if (!st) return "reject";
|
|
16476
|
+
if (st.authed) return "pass";
|
|
16477
|
+
const parsed = AuthRequestFrameSchema.safeParse(frame);
|
|
16478
|
+
if (!parsed.success) {
|
|
16479
|
+
const reason = frame.type === "auth" ? "auth failed" : "auth required";
|
|
16480
|
+
this.opts.closeConnection(handle, 1008, reason);
|
|
16481
|
+
this.markFailed(handle.id);
|
|
16482
|
+
return frame.type === "auth" ? "consumed" : "reject";
|
|
16483
|
+
}
|
|
16484
|
+
if (!this.opts.expectedToken) {
|
|
16485
|
+
this.opts.closeConnection(handle, 1008, "auth not configured");
|
|
16486
|
+
this.markFailed(handle.id);
|
|
16487
|
+
return "consumed";
|
|
16488
|
+
}
|
|
16489
|
+
if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
|
|
16490
|
+
this.opts.closeConnection(handle, 1008, "auth failed");
|
|
16491
|
+
this.markFailed(handle.id);
|
|
16492
|
+
return "consumed";
|
|
16493
|
+
}
|
|
16494
|
+
if (st.timer != null) {
|
|
16495
|
+
const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
16496
|
+
clear(st.timer);
|
|
16497
|
+
}
|
|
16498
|
+
st.authed = true;
|
|
16499
|
+
st.timer = null;
|
|
16500
|
+
this.opts.sendOk(handle, { type: "auth:ok" });
|
|
16501
|
+
return "consumed";
|
|
16502
|
+
}
|
|
16503
|
+
markFailed(handleId) {
|
|
16504
|
+
const st = this.states.get(handleId);
|
|
16505
|
+
if (!st) return;
|
|
16506
|
+
if (st.timer != null) {
|
|
16507
|
+
const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
16508
|
+
clear(st.timer);
|
|
16509
|
+
}
|
|
16510
|
+
st.timer = null;
|
|
16511
|
+
}
|
|
16512
|
+
};
|
|
16513
|
+
function constantTimeEqual(a, b) {
|
|
16514
|
+
if (a.length !== b.length) return false;
|
|
16515
|
+
let diff2 = 0;
|
|
16516
|
+
for (let i = 0; i < a.length; i++) diff2 |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
16517
|
+
return diff2 === 0;
|
|
16518
|
+
}
|
|
16519
|
+
function buildShouldEnforce(opts) {
|
|
16520
|
+
if (opts.tunnel) return () => true;
|
|
16521
|
+
return (addr) => !isLocalhost(addr);
|
|
16522
|
+
}
|
|
16523
|
+
function isLocalhost(addr) {
|
|
16524
|
+
if (!addr) return false;
|
|
16525
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
16526
|
+
}
|
|
16527
|
+
|
|
16343
16528
|
// src/discovery/state-file.ts
|
|
16344
16529
|
var import_node_fs10 = __toESM(require("fs"), 1);
|
|
16345
16530
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
@@ -16399,17 +16584,532 @@ var StateFileManager = class {
|
|
|
16399
16584
|
}
|
|
16400
16585
|
};
|
|
16401
16586
|
|
|
16402
|
-
// src/
|
|
16403
|
-
var
|
|
16587
|
+
// src/tunnel/tunnel-manager.ts
|
|
16588
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
16589
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
16590
|
+
var import_node_child_process4 = require("child_process");
|
|
16591
|
+
|
|
16592
|
+
// src/tunnel/tunnel-store.ts
|
|
16404
16593
|
var import_node_fs11 = __toESM(require("fs"), 1);
|
|
16405
|
-
var import_node_os7 = __toESM(require("os"), 1);
|
|
16406
16594
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
16595
|
+
var TunnelStore = class {
|
|
16596
|
+
constructor(filePath) {
|
|
16597
|
+
this.filePath = filePath;
|
|
16598
|
+
}
|
|
16599
|
+
filePath;
|
|
16600
|
+
async get() {
|
|
16601
|
+
try {
|
|
16602
|
+
const raw = await import_node_fs11.default.promises.readFile(this.filePath, "utf8");
|
|
16603
|
+
const obj = JSON.parse(raw);
|
|
16604
|
+
if (!isPersistedTunnel(obj)) return null;
|
|
16605
|
+
return obj;
|
|
16606
|
+
} catch (err) {
|
|
16607
|
+
const code = err?.code;
|
|
16608
|
+
if (code === "ENOENT") return null;
|
|
16609
|
+
return null;
|
|
16610
|
+
}
|
|
16611
|
+
}
|
|
16612
|
+
async set(v) {
|
|
16613
|
+
const dir = import_node_path10.default.dirname(this.filePath);
|
|
16614
|
+
await import_node_fs11.default.promises.mkdir(dir, { recursive: true });
|
|
16615
|
+
const data = JSON.stringify(v, null, 2);
|
|
16616
|
+
const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
16617
|
+
await import_node_fs11.default.promises.writeFile(tmp, data, { mode: 384 });
|
|
16618
|
+
if (process.platform !== "win32") {
|
|
16619
|
+
try {
|
|
16620
|
+
await import_node_fs11.default.promises.chmod(tmp, 384);
|
|
16621
|
+
} catch {
|
|
16622
|
+
}
|
|
16623
|
+
}
|
|
16624
|
+
await import_node_fs11.default.promises.rename(tmp, this.filePath);
|
|
16625
|
+
}
|
|
16626
|
+
async clear() {
|
|
16627
|
+
try {
|
|
16628
|
+
await import_node_fs11.default.promises.unlink(this.filePath);
|
|
16629
|
+
} catch (err) {
|
|
16630
|
+
const code = err?.code;
|
|
16631
|
+
if (code !== "ENOENT") throw err;
|
|
16632
|
+
}
|
|
16633
|
+
}
|
|
16634
|
+
};
|
|
16635
|
+
function isPersistedTunnel(o) {
|
|
16636
|
+
if (!o || typeof o !== "object") return false;
|
|
16637
|
+
const r = o;
|
|
16638
|
+
return typeof r.subdomain === "string" && typeof r.frpsHost === "string" && typeof r.frpsPort === "number" && typeof r.frpsToken === "string" && typeof r.url === "string" && typeof r.registeredAt === "string";
|
|
16639
|
+
}
|
|
16640
|
+
|
|
16641
|
+
// src/tunnel/register-client.ts
|
|
16642
|
+
var TunnelRegisterError = class extends Error {
|
|
16643
|
+
constructor(message, cause) {
|
|
16644
|
+
super(message);
|
|
16645
|
+
this.cause = cause;
|
|
16646
|
+
this.name = "TunnelRegisterError";
|
|
16647
|
+
}
|
|
16648
|
+
cause;
|
|
16649
|
+
};
|
|
16650
|
+
async function registerTunnel(opts) {
|
|
16651
|
+
const f = opts.fetchImpl ?? globalThis.fetch;
|
|
16652
|
+
if (!f) throw new TunnelRegisterError("fetch is not available (Node >= 18 required)");
|
|
16653
|
+
const url = `${opts.api.replace(/\/+$/, "")}/api/tunnel/register`;
|
|
16654
|
+
const ctrl = new AbortController();
|
|
16655
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
|
|
16656
|
+
let res;
|
|
16657
|
+
try {
|
|
16658
|
+
res = await f(url, {
|
|
16659
|
+
method: "POST",
|
|
16660
|
+
headers: { "content-type": "application/json" },
|
|
16661
|
+
body: JSON.stringify({ gatewayCount: 1, deviceKeys: [opts.deviceKey] }),
|
|
16662
|
+
signal: ctrl.signal
|
|
16663
|
+
});
|
|
16664
|
+
} catch (err) {
|
|
16665
|
+
clearTimeout(timer);
|
|
16666
|
+
throw new TunnelRegisterError(`tunnel register network error: ${err.message}`, err);
|
|
16667
|
+
}
|
|
16668
|
+
clearTimeout(timer);
|
|
16669
|
+
if (!res.ok) {
|
|
16670
|
+
let bodyText = "";
|
|
16671
|
+
try {
|
|
16672
|
+
bodyText = (await res.text()).slice(0, 200);
|
|
16673
|
+
} catch {
|
|
16674
|
+
}
|
|
16675
|
+
throw new TunnelRegisterError(`tunnel register failed: HTTP ${res.status} ${bodyText}`);
|
|
16676
|
+
}
|
|
16677
|
+
let payload;
|
|
16678
|
+
try {
|
|
16679
|
+
payload = await res.json();
|
|
16680
|
+
} catch (err) {
|
|
16681
|
+
throw new TunnelRegisterError(`tunnel register: invalid json response`, err);
|
|
16682
|
+
}
|
|
16683
|
+
const tunnels = payload.tunnels;
|
|
16684
|
+
if (!Array.isArray(tunnels) || tunnels.length === 0) {
|
|
16685
|
+
throw new TunnelRegisterError(`tunnel register: missing tunnels in response`);
|
|
16686
|
+
}
|
|
16687
|
+
const t = tunnels[0];
|
|
16688
|
+
if (typeof t.subdomain !== "string" || typeof t.url !== "string" || typeof t.frpsHost !== "string" || typeof t.frpsPort !== "number" || typeof t.token !== "string") {
|
|
16689
|
+
throw new TunnelRegisterError(`tunnel register: malformed tunnel entry`);
|
|
16690
|
+
}
|
|
16691
|
+
return {
|
|
16692
|
+
subdomain: t.subdomain,
|
|
16693
|
+
url: t.url,
|
|
16694
|
+
frpsHost: t.frpsHost,
|
|
16695
|
+
frpsPort: t.frpsPort,
|
|
16696
|
+
frpsToken: t.token
|
|
16697
|
+
};
|
|
16698
|
+
}
|
|
16699
|
+
|
|
16700
|
+
// src/tunnel/frpc-config.ts
|
|
16701
|
+
function buildFrpcToml(input) {
|
|
16702
|
+
const localIP = input.localIP ?? "127.0.0.1";
|
|
16703
|
+
const lines = [];
|
|
16704
|
+
lines.push(`serverAddr = "${escape(input.serverAddr)}"`);
|
|
16705
|
+
lines.push(`serverPort = ${input.serverPort}`);
|
|
16706
|
+
lines.push(`auth.method = "token"`);
|
|
16707
|
+
lines.push(`auth.token = "${escape(input.authToken)}"`);
|
|
16708
|
+
lines.push(`log.level = "${input.logLevel ?? "info"}"`);
|
|
16709
|
+
lines.push("");
|
|
16710
|
+
lines.push("[[proxies]]");
|
|
16711
|
+
lines.push(`name = "${escape(input.proxyName)}"`);
|
|
16712
|
+
lines.push('type = "http"');
|
|
16713
|
+
lines.push(`localIP = "${escape(localIP)}"`);
|
|
16714
|
+
lines.push(`localPort = ${input.localPort}`);
|
|
16715
|
+
lines.push(`subdomain = "${escape(input.subdomain)}"`);
|
|
16716
|
+
return lines.join("\n") + "\n";
|
|
16717
|
+
}
|
|
16718
|
+
function escape(v) {
|
|
16719
|
+
return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
16720
|
+
}
|
|
16721
|
+
|
|
16722
|
+
// src/tunnel/frpc-binary.ts
|
|
16723
|
+
var import_node_fs12 = __toESM(require("fs"), 1);
|
|
16724
|
+
var import_node_os7 = __toESM(require("os"), 1);
|
|
16725
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
16726
|
+
var import_node_child_process3 = require("child_process");
|
|
16727
|
+
var import_node_stream = require("stream");
|
|
16728
|
+
var import_promises = require("stream/promises");
|
|
16729
|
+
var FRPC_VERSION = "0.68.0";
|
|
16730
|
+
var UnsupportedPlatformError = class extends Error {
|
|
16731
|
+
constructor(platform, arch) {
|
|
16732
|
+
super(`unsupported frpc platform: ${platform}/${arch}`);
|
|
16733
|
+
this.platform = platform;
|
|
16734
|
+
this.arch = arch;
|
|
16735
|
+
}
|
|
16736
|
+
platform;
|
|
16737
|
+
arch;
|
|
16738
|
+
};
|
|
16739
|
+
function detectPlatform(opts = {}) {
|
|
16740
|
+
const platform = opts.platform ?? process.platform;
|
|
16741
|
+
const arch = opts.arch ?? process.arch;
|
|
16742
|
+
let oss;
|
|
16743
|
+
if (platform === "darwin") oss = "darwin";
|
|
16744
|
+
else if (platform === "linux") oss = "linux";
|
|
16745
|
+
else throw new UnsupportedPlatformError(platform, arch);
|
|
16746
|
+
let a;
|
|
16747
|
+
if (arch === "arm64" || arch === "aarch64") a = "arm64";
|
|
16748
|
+
else if (arch === "x64" || arch === "amd64") a = "amd64";
|
|
16749
|
+
else throw new UnsupportedPlatformError(platform, arch);
|
|
16750
|
+
return { os: oss, arch: a };
|
|
16751
|
+
}
|
|
16752
|
+
function frpcDownloadUrl(version2, p) {
|
|
16753
|
+
return `https://github.com/fatedier/frp/releases/download/v${version2}/frp_${version2}_${p.os}_${p.arch}.tar.gz`;
|
|
16754
|
+
}
|
|
16755
|
+
async function ensureFrpcBinary(opts) {
|
|
16756
|
+
if (opts.override) {
|
|
16757
|
+
if (!import_node_fs12.default.existsSync(opts.override)) {
|
|
16758
|
+
throw new Error(`frpc binary not found at override path: ${opts.override}`);
|
|
16759
|
+
}
|
|
16760
|
+
return opts.override;
|
|
16761
|
+
}
|
|
16762
|
+
const version2 = opts.version ?? FRPC_VERSION;
|
|
16763
|
+
const platform = opts.platform ?? detectPlatform();
|
|
16764
|
+
const binDir = import_node_path11.default.join(opts.dataDir, "bin");
|
|
16765
|
+
import_node_fs12.default.mkdirSync(binDir, { recursive: true });
|
|
16766
|
+
cleanupStaleArtifacts(binDir);
|
|
16767
|
+
const stableBin = import_node_path11.default.join(binDir, "frpc");
|
|
16768
|
+
if (import_node_fs12.default.existsSync(stableBin)) return stableBin;
|
|
16769
|
+
const partialBin = `${stableBin}.partial`;
|
|
16770
|
+
const tarballPath = import_node_path11.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
|
|
16771
|
+
try {
|
|
16772
|
+
const url = frpcDownloadUrl(version2, platform);
|
|
16773
|
+
await downloadToFile(url, tarballPath, opts.fetchImpl);
|
|
16774
|
+
if (opts.installFromTarball) {
|
|
16775
|
+
await opts.installFromTarball(tarballPath, partialBin);
|
|
16776
|
+
} else {
|
|
16777
|
+
await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
|
|
16778
|
+
}
|
|
16779
|
+
import_node_fs12.default.chmodSync(partialBin, 493);
|
|
16780
|
+
import_node_fs12.default.renameSync(partialBin, stableBin);
|
|
16781
|
+
} finally {
|
|
16782
|
+
safeUnlink(tarballPath);
|
|
16783
|
+
safeUnlink(partialBin);
|
|
16784
|
+
}
|
|
16785
|
+
return stableBin;
|
|
16786
|
+
}
|
|
16787
|
+
function cleanupStaleArtifacts(binDir) {
|
|
16788
|
+
let entries;
|
|
16789
|
+
try {
|
|
16790
|
+
entries = import_node_fs12.default.readdirSync(binDir);
|
|
16791
|
+
} catch {
|
|
16792
|
+
return;
|
|
16793
|
+
}
|
|
16794
|
+
for (const name of entries) {
|
|
16795
|
+
if (name.endsWith(".partial") || name.startsWith("extract-")) {
|
|
16796
|
+
const full = import_node_path11.default.join(binDir, name);
|
|
16797
|
+
try {
|
|
16798
|
+
import_node_fs12.default.rmSync(full, { recursive: true, force: true });
|
|
16799
|
+
} catch {
|
|
16800
|
+
}
|
|
16801
|
+
}
|
|
16802
|
+
}
|
|
16803
|
+
}
|
|
16804
|
+
function safeUnlink(p) {
|
|
16805
|
+
try {
|
|
16806
|
+
import_node_fs12.default.unlinkSync(p);
|
|
16807
|
+
} catch {
|
|
16808
|
+
}
|
|
16809
|
+
}
|
|
16810
|
+
async function downloadToFile(url, dest, fetchImpl) {
|
|
16811
|
+
const f = fetchImpl ?? globalThis.fetch;
|
|
16812
|
+
if (!f) throw new Error("fetch is not available (Node >= 18 required)");
|
|
16813
|
+
const res = await f(url, { redirect: "follow" });
|
|
16814
|
+
if (!res.ok || !res.body) {
|
|
16815
|
+
throw new Error(`download failed: ${res.status} ${res.statusText}`);
|
|
16816
|
+
}
|
|
16817
|
+
const out = import_node_fs12.default.createWriteStream(dest);
|
|
16818
|
+
const nodeStream = import_node_stream.Readable.fromWeb(res.body);
|
|
16819
|
+
await (0, import_promises.pipeline)(nodeStream, out);
|
|
16820
|
+
}
|
|
16821
|
+
async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
|
|
16822
|
+
const work = import_node_path11.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
|
|
16823
|
+
import_node_fs12.default.mkdirSync(work, { recursive: true });
|
|
16824
|
+
try {
|
|
16825
|
+
await new Promise((resolve, reject) => {
|
|
16826
|
+
const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
|
|
16827
|
+
proc.on("error", reject);
|
|
16828
|
+
proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`tar exited ${code}`)));
|
|
16829
|
+
});
|
|
16830
|
+
const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
|
|
16831
|
+
const src = import_node_path11.default.join(work, dirName, "frpc");
|
|
16832
|
+
if (!import_node_fs12.default.existsSync(src)) {
|
|
16833
|
+
throw new Error(`frpc not found inside tarball at ${src}`);
|
|
16834
|
+
}
|
|
16835
|
+
import_node_fs12.default.copyFileSync(src, destBin);
|
|
16836
|
+
} finally {
|
|
16837
|
+
import_node_fs12.default.rmSync(work, { recursive: true, force: true });
|
|
16838
|
+
}
|
|
16839
|
+
}
|
|
16840
|
+
|
|
16841
|
+
// src/tunnel/tunnel-manager.ts
|
|
16842
|
+
var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
16843
|
+
var TunnelManager = class {
|
|
16844
|
+
constructor(deps) {
|
|
16845
|
+
this.deps = deps;
|
|
16846
|
+
this.store = deps.store ?? new TunnelStore(import_node_path12.default.join(deps.dataDir, "tunnel.json"));
|
|
16847
|
+
this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
|
|
16848
|
+
this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
|
|
16849
|
+
}
|
|
16850
|
+
deps;
|
|
16851
|
+
proc = null;
|
|
16852
|
+
current = null;
|
|
16853
|
+
store;
|
|
16854
|
+
ttlMs;
|
|
16855
|
+
startupTimeoutMs;
|
|
16856
|
+
exitHookInstalled = false;
|
|
16857
|
+
// 标记是否在主动 stop 中:true 时不要把 frpc 退出 emit 成 onFrpcExit
|
|
16858
|
+
stopping = false;
|
|
16859
|
+
get url() {
|
|
16860
|
+
return this.current?.url ?? null;
|
|
16861
|
+
}
|
|
16862
|
+
async start(opts) {
|
|
16863
|
+
const { logger } = this.deps;
|
|
16864
|
+
if (!this.deps.noPersist) {
|
|
16865
|
+
const cached = await this.store.get();
|
|
16866
|
+
if (cached && this.isFresh(cached)) {
|
|
16867
|
+
logger?.info("tunnel: trying cached credentials", { subdomain: cached.subdomain });
|
|
16868
|
+
const r2 = await this.tryStart(cached, opts.localPort);
|
|
16869
|
+
if (r2.ok) {
|
|
16870
|
+
this.proc = r2.proc;
|
|
16871
|
+
this.current = cached;
|
|
16872
|
+
this.installProcessExitHandlers();
|
|
16873
|
+
this.attachExitListener(r2.proc);
|
|
16874
|
+
return { url: cached.url, subdomain: cached.subdomain };
|
|
16875
|
+
}
|
|
16876
|
+
logger?.warn("tunnel: cached credentials failed, re-registering", { err: r2.error.message });
|
|
16877
|
+
}
|
|
16878
|
+
}
|
|
16879
|
+
const reg = await (this.deps.registerImpl ?? registerTunnel)({
|
|
16880
|
+
api: this.deps.clawosApi,
|
|
16881
|
+
deviceKey: this.deps.deviceKey
|
|
16882
|
+
});
|
|
16883
|
+
const persisted = {
|
|
16884
|
+
subdomain: reg.subdomain,
|
|
16885
|
+
frpsHost: reg.frpsHost,
|
|
16886
|
+
frpsPort: reg.frpsPort,
|
|
16887
|
+
frpsToken: reg.frpsToken,
|
|
16888
|
+
url: reg.url,
|
|
16889
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16890
|
+
};
|
|
16891
|
+
await this.store.set(persisted);
|
|
16892
|
+
const r = await this.tryStart(persisted, opts.localPort);
|
|
16893
|
+
if (!r.ok) throw r.error;
|
|
16894
|
+
this.proc = r.proc;
|
|
16895
|
+
this.current = persisted;
|
|
16896
|
+
this.installProcessExitHandlers();
|
|
16897
|
+
this.attachExitListener(r.proc);
|
|
16898
|
+
return { url: persisted.url, subdomain: persisted.subdomain };
|
|
16899
|
+
}
|
|
16900
|
+
async stop() {
|
|
16901
|
+
this.stopping = true;
|
|
16902
|
+
const proc = this.proc;
|
|
16903
|
+
this.proc = null;
|
|
16904
|
+
if (!proc) return;
|
|
16905
|
+
proc.kill("SIGTERM");
|
|
16906
|
+
await new Promise((resolve) => {
|
|
16907
|
+
const t = setTimeout(() => {
|
|
16908
|
+
try {
|
|
16909
|
+
proc.kill("SIGKILL");
|
|
16910
|
+
} catch {
|
|
16911
|
+
}
|
|
16912
|
+
resolve();
|
|
16913
|
+
}, 5e3);
|
|
16914
|
+
proc.once("exit", () => {
|
|
16915
|
+
clearTimeout(t);
|
|
16916
|
+
resolve();
|
|
16917
|
+
});
|
|
16918
|
+
});
|
|
16919
|
+
}
|
|
16920
|
+
// 同步杀子进程:父进程崩溃 / SIGHUP / process.exit 路径下使用,
|
|
16921
|
+
// async stop() 跑不完时的兜底,避免留下孤儿 frpc。
|
|
16922
|
+
killSync() {
|
|
16923
|
+
const proc = this.proc;
|
|
16924
|
+
this.proc = null;
|
|
16925
|
+
if (!proc) return;
|
|
16926
|
+
try {
|
|
16927
|
+
proc.kill("SIGTERM");
|
|
16928
|
+
} catch {
|
|
16929
|
+
}
|
|
16930
|
+
}
|
|
16931
|
+
isFresh(t) {
|
|
16932
|
+
const age = Date.now() - new Date(t.registeredAt).getTime();
|
|
16933
|
+
return Number.isFinite(age) && age < this.ttlMs;
|
|
16934
|
+
}
|
|
16935
|
+
attachExitListener(proc) {
|
|
16936
|
+
proc.on("exit", (code) => {
|
|
16937
|
+
this.deps.logger?.warn("frpc exited", { code });
|
|
16938
|
+
if (this.stopping) return;
|
|
16939
|
+
const cur = this.current;
|
|
16940
|
+
this.proc = null;
|
|
16941
|
+
this.deps.onFrpcExit?.({
|
|
16942
|
+
code,
|
|
16943
|
+
subdomain: cur?.subdomain ?? null,
|
|
16944
|
+
url: cur?.url ?? null
|
|
16945
|
+
});
|
|
16946
|
+
});
|
|
16947
|
+
}
|
|
16948
|
+
installProcessExitHandlers() {
|
|
16949
|
+
if (this.exitHookInstalled) return;
|
|
16950
|
+
if (this.deps.installProcessExitHandlers !== true) return;
|
|
16951
|
+
this.exitHookInstalled = true;
|
|
16952
|
+
const sync = () => this.killSync();
|
|
16953
|
+
process.once("exit", sync);
|
|
16954
|
+
process.once("SIGHUP", sync);
|
|
16955
|
+
process.once("uncaughtException", sync);
|
|
16956
|
+
}
|
|
16957
|
+
async tryStart(t, localPort) {
|
|
16958
|
+
try {
|
|
16959
|
+
const frpcBin = await (this.deps.ensureFrpcImpl ?? ensureFrpcBinary)({
|
|
16960
|
+
dataDir: this.deps.dataDir,
|
|
16961
|
+
override: this.deps.frpcBinaryOverride ?? void 0
|
|
16962
|
+
});
|
|
16963
|
+
const tomlPath = import_node_path12.default.join(this.deps.dataDir, "frpc.toml");
|
|
16964
|
+
const toml = buildFrpcToml({
|
|
16965
|
+
serverAddr: t.frpsHost,
|
|
16966
|
+
serverPort: t.frpsPort,
|
|
16967
|
+
authToken: t.frpsToken,
|
|
16968
|
+
proxyName: "clawd",
|
|
16969
|
+
subdomain: t.subdomain,
|
|
16970
|
+
localPort,
|
|
16971
|
+
logLevel: "info"
|
|
16972
|
+
});
|
|
16973
|
+
await import_node_fs13.default.promises.writeFile(tomlPath, toml, { mode: 384 });
|
|
16974
|
+
const proc = (this.deps.spawnImpl ?? import_node_child_process4.spawn)(frpcBin, ["-c", tomlPath], {
|
|
16975
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
16976
|
+
});
|
|
16977
|
+
const logFilePath = import_node_path12.default.join(this.deps.dataDir, "frpc.log");
|
|
16978
|
+
const logStream = import_node_fs13.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
|
|
16979
|
+
logStream.on("error", () => {
|
|
16980
|
+
});
|
|
16981
|
+
const tee = (chunk) => {
|
|
16982
|
+
logStream.write(String(chunk));
|
|
16983
|
+
};
|
|
16984
|
+
proc.stdout?.on("data", tee);
|
|
16985
|
+
proc.stderr?.on("data", tee);
|
|
16986
|
+
proc.once("exit", () => {
|
|
16987
|
+
logStream.end();
|
|
16988
|
+
});
|
|
16989
|
+
const ready = await waitForFrpcReady(proc, this.startupTimeoutMs);
|
|
16990
|
+
if (!ready.ok) {
|
|
16991
|
+
try {
|
|
16992
|
+
proc.kill("SIGTERM");
|
|
16993
|
+
} catch {
|
|
16994
|
+
}
|
|
16995
|
+
return { ok: false, error: new Error(ready.error) };
|
|
16996
|
+
}
|
|
16997
|
+
this.deps.logger?.info("tunnel: frpc up", { subdomain: t.subdomain, url: t.url });
|
|
16998
|
+
return { ok: true, proc };
|
|
16999
|
+
} catch (err) {
|
|
17000
|
+
if (err instanceof TunnelRegisterError) throw err;
|
|
17001
|
+
return { ok: false, error: err };
|
|
17002
|
+
}
|
|
17003
|
+
}
|
|
17004
|
+
};
|
|
17005
|
+
async function waitForFrpcReady(proc, timeoutMs) {
|
|
17006
|
+
return new Promise((resolve) => {
|
|
17007
|
+
let settled = false;
|
|
17008
|
+
let buf = "";
|
|
17009
|
+
const finish = (r) => {
|
|
17010
|
+
if (settled) return;
|
|
17011
|
+
settled = true;
|
|
17012
|
+
cleanup();
|
|
17013
|
+
resolve(r);
|
|
17014
|
+
};
|
|
17015
|
+
const onData = (chunk) => {
|
|
17016
|
+
buf += String(chunk);
|
|
17017
|
+
if (/start proxy success/.test(buf) || /\[clawd\] success/.test(buf)) {
|
|
17018
|
+
finish({ ok: true });
|
|
17019
|
+
}
|
|
17020
|
+
if (/login to server failed|authentication failed/i.test(buf)) {
|
|
17021
|
+
finish({ ok: false, error: `frpc handshake failed: ${buf.trim().slice(-200)}` });
|
|
17022
|
+
}
|
|
17023
|
+
};
|
|
17024
|
+
const onExit = (code) => {
|
|
17025
|
+
finish({ ok: false, error: `frpc exited before ready (code=${code})` });
|
|
17026
|
+
};
|
|
17027
|
+
const onErr = (err) => finish({ ok: false, error: `frpc spawn error: ${err.message}` });
|
|
17028
|
+
const cleanup = () => {
|
|
17029
|
+
proc.stdout?.off("data", onData);
|
|
17030
|
+
proc.stderr?.off("data", onData);
|
|
17031
|
+
proc.off("exit", onExit);
|
|
17032
|
+
proc.off("error", onErr);
|
|
17033
|
+
clearTimeout(timer);
|
|
17034
|
+
};
|
|
17035
|
+
proc.stdout?.on("data", onData);
|
|
17036
|
+
proc.stderr?.on("data", onData);
|
|
17037
|
+
proc.on("exit", onExit);
|
|
17038
|
+
proc.on("error", onErr);
|
|
17039
|
+
const timer = setTimeout(() => {
|
|
17040
|
+
finish({ ok: false, error: `frpc startup timeout after ${timeoutMs}ms` });
|
|
17041
|
+
}, timeoutMs);
|
|
17042
|
+
});
|
|
17043
|
+
}
|
|
17044
|
+
|
|
17045
|
+
// src/tunnel/device-key.ts
|
|
17046
|
+
var import_node_os8 = __toESM(require("os"), 1);
|
|
17047
|
+
var import_node_crypto3 = __toESM(require("crypto"), 1);
|
|
17048
|
+
var DERIVE_SALT = "clawd-tunnel-device-v1";
|
|
17049
|
+
function deriveStableDeviceKey(opts = {}) {
|
|
17050
|
+
const hostname = opts.hostname ?? import_node_os8.default.hostname();
|
|
17051
|
+
const uid = opts.uid ?? (typeof import_node_os8.default.userInfo === "function" ? import_node_os8.default.userInfo().uid : 0);
|
|
17052
|
+
const input = `${hostname}::${uid}`;
|
|
17053
|
+
return import_node_crypto3.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
|
|
17054
|
+
}
|
|
17055
|
+
|
|
17056
|
+
// src/auth-store.ts
|
|
17057
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
17058
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
17059
|
+
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
17060
|
+
var AUTH_FILE_NAME = "auth.json";
|
|
17061
|
+
function authFilePath(dataDir) {
|
|
17062
|
+
return import_node_path13.default.join(dataDir, AUTH_FILE_NAME);
|
|
17063
|
+
}
|
|
17064
|
+
function loadOrCreateAuthToken(opts) {
|
|
17065
|
+
const file = authFilePath(opts.dataDir);
|
|
17066
|
+
const existing = readAuthFile(file);
|
|
17067
|
+
if (existing && existing.token) return existing.token;
|
|
17068
|
+
const token = (opts.generate ?? defaultGenerate)();
|
|
17069
|
+
const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
17070
|
+
writeAuthFile(file, { token, createdAt: now.toISOString() });
|
|
17071
|
+
return token;
|
|
17072
|
+
}
|
|
17073
|
+
function defaultGenerate() {
|
|
17074
|
+
return import_node_crypto4.default.randomBytes(32).toString("base64url");
|
|
17075
|
+
}
|
|
17076
|
+
function readAuthFile(file) {
|
|
17077
|
+
try {
|
|
17078
|
+
const raw = import_node_fs14.default.readFileSync(file, "utf8");
|
|
17079
|
+
const parsed = JSON.parse(raw);
|
|
17080
|
+
if (typeof parsed?.token === "string" && parsed.token.length > 0) {
|
|
17081
|
+
return {
|
|
17082
|
+
token: parsed.token,
|
|
17083
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
17084
|
+
};
|
|
17085
|
+
}
|
|
17086
|
+
return null;
|
|
17087
|
+
} catch (err) {
|
|
17088
|
+
const code = err?.code;
|
|
17089
|
+
if (code === "ENOENT") return null;
|
|
17090
|
+
return null;
|
|
17091
|
+
}
|
|
17092
|
+
}
|
|
17093
|
+
function writeAuthFile(file, content) {
|
|
17094
|
+
import_node_fs14.default.mkdirSync(import_node_path13.default.dirname(file), { recursive: true });
|
|
17095
|
+
import_node_fs14.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
|
|
17096
|
+
try {
|
|
17097
|
+
import_node_fs14.default.chmodSync(file, 384);
|
|
17098
|
+
} catch {
|
|
17099
|
+
}
|
|
17100
|
+
}
|
|
17101
|
+
|
|
17102
|
+
// src/workspace/git.ts
|
|
17103
|
+
var import_node_child_process5 = require("child_process");
|
|
17104
|
+
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
17105
|
+
var import_node_os9 = __toESM(require("os"), 1);
|
|
17106
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
16407
17107
|
var import_node_util = require("util");
|
|
16408
|
-
var pexec = (0, import_node_util.promisify)(
|
|
17108
|
+
var pexec = (0, import_node_util.promisify)(import_node_child_process5.execFile);
|
|
16409
17109
|
function normalizePath(p) {
|
|
16410
|
-
const resolved =
|
|
17110
|
+
const resolved = import_node_path14.default.resolve(p);
|
|
16411
17111
|
try {
|
|
16412
|
-
return
|
|
17112
|
+
return import_node_fs15.default.realpathSync(resolved);
|
|
16413
17113
|
} catch {
|
|
16414
17114
|
return resolved;
|
|
16415
17115
|
}
|
|
@@ -16496,7 +17196,7 @@ function sanitizeLabel(raw) {
|
|
|
16496
17196
|
function computePrefix() {
|
|
16497
17197
|
let username;
|
|
16498
17198
|
try {
|
|
16499
|
-
username =
|
|
17199
|
+
username = import_node_os9.default.userInfo().username;
|
|
16500
17200
|
} catch {
|
|
16501
17201
|
username = void 0;
|
|
16502
17202
|
}
|
|
@@ -16510,13 +17210,13 @@ function flattenToDirName(branch) {
|
|
|
16510
17210
|
}
|
|
16511
17211
|
function encodeClaudeProjectDir(absPath) {
|
|
16512
17212
|
if (!absPath || typeof absPath !== "string") return "";
|
|
16513
|
-
let canonical =
|
|
17213
|
+
let canonical = import_node_path14.default.resolve(absPath);
|
|
16514
17214
|
try {
|
|
16515
|
-
canonical =
|
|
17215
|
+
canonical = import_node_fs15.default.realpathSync(canonical);
|
|
16516
17216
|
} catch {
|
|
16517
17217
|
try {
|
|
16518
|
-
const parent =
|
|
16519
|
-
canonical =
|
|
17218
|
+
const parent = import_node_fs15.default.realpathSync(import_node_path14.default.dirname(canonical));
|
|
17219
|
+
canonical = import_node_path14.default.join(parent, import_node_path14.default.basename(canonical));
|
|
16520
17220
|
} catch {
|
|
16521
17221
|
}
|
|
16522
17222
|
}
|
|
@@ -16540,11 +17240,11 @@ async function createWorktree(input) {
|
|
|
16540
17240
|
if (!isGitRoot) {
|
|
16541
17241
|
throw new Error(`\u76EE\u5F55 ${cwd} \u4E0D\u662F git repo \u6839`);
|
|
16542
17242
|
}
|
|
16543
|
-
const parent =
|
|
16544
|
-
if (parent === "/" || parent ===
|
|
17243
|
+
const parent = import_node_path14.default.dirname(import_node_path14.default.resolve(cwd));
|
|
17244
|
+
if (parent === "/" || parent === import_node_path14.default.resolve(cwd)) {
|
|
16545
17245
|
throw new Error("repo \u5728\u78C1\u76D8\u6839\u76EE\u5F55\uFF0C\u65E0\u6CD5\u5728\u540C\u7EA7\u521B\u5EFA worktree");
|
|
16546
17246
|
}
|
|
16547
|
-
const worktreeRoot =
|
|
17247
|
+
const worktreeRoot = import_node_path14.default.join(parent, dirName);
|
|
16548
17248
|
try {
|
|
16549
17249
|
await pexec("git", ["-C", cwd, "rev-parse", "--verify", `refs/heads/${baseBranch}`], {
|
|
16550
17250
|
timeout: 3e3
|
|
@@ -16561,7 +17261,7 @@ async function createWorktree(input) {
|
|
|
16561
17261
|
const msg = err.message;
|
|
16562
17262
|
if (msg.startsWith("\u5206\u652F ")) throw err;
|
|
16563
17263
|
}
|
|
16564
|
-
if (
|
|
17264
|
+
if (import_node_fs15.default.existsSync(worktreeRoot)) {
|
|
16565
17265
|
throw new Error(`\u76EE\u5F55 ${worktreeRoot} \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E00\u4E2A label \u6216\u6E05\u7406\u540E\u91CD\u8BD5`);
|
|
16566
17266
|
}
|
|
16567
17267
|
try {
|
|
@@ -16591,8 +17291,8 @@ async function removeWorktree(input) {
|
|
|
16591
17291
|
);
|
|
16592
17292
|
const gitCommonDir = stdout.trim();
|
|
16593
17293
|
if (!gitCommonDir) throw new Error("empty git-common-dir");
|
|
16594
|
-
const absGitCommon =
|
|
16595
|
-
repoRoot =
|
|
17294
|
+
const absGitCommon = import_node_path14.default.isAbsolute(gitCommonDir) ? gitCommonDir : import_node_path14.default.resolve(worktreeRoot, gitCommonDir);
|
|
17295
|
+
repoRoot = import_node_path14.default.dirname(absGitCommon);
|
|
16596
17296
|
} catch {
|
|
16597
17297
|
repoRoot = null;
|
|
16598
17298
|
}
|
|
@@ -16604,7 +17304,7 @@ async function removeWorktree(input) {
|
|
|
16604
17304
|
} catch (err) {
|
|
16605
17305
|
const stderr = err.stderr || err.message;
|
|
16606
17306
|
const lower = stderr.toLowerCase();
|
|
16607
|
-
const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !
|
|
17307
|
+
const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs15.default.existsSync(worktreeRoot);
|
|
16608
17308
|
if (!vanished) {
|
|
16609
17309
|
const firstLine = stderr.split("\n")[0].trim();
|
|
16610
17310
|
throw new Error(`\u6E05\u7406 worktree \u5931\u8D25\uFF1A${firstLine}`);
|
|
@@ -16625,10 +17325,10 @@ async function removeWorktree(input) {
|
|
|
16625
17325
|
try {
|
|
16626
17326
|
const encoded = encodeClaudeProjectDir(worktreeRoot);
|
|
16627
17327
|
if (encoded) {
|
|
16628
|
-
const projectsRoot =
|
|
16629
|
-
const target =
|
|
16630
|
-
if (target.startsWith(projectsRoot +
|
|
16631
|
-
|
|
17328
|
+
const projectsRoot = import_node_path14.default.join(import_node_os9.default.homedir(), ".claude", "projects");
|
|
17329
|
+
const target = import_node_path14.default.resolve(projectsRoot, encoded);
|
|
17330
|
+
if (target.startsWith(projectsRoot + import_node_path14.default.sep) && target !== projectsRoot) {
|
|
17331
|
+
import_node_fs15.default.rmSync(target, { recursive: true, force: true });
|
|
16632
17332
|
}
|
|
16633
17333
|
}
|
|
16634
17334
|
} catch {
|
|
@@ -16650,13 +17350,13 @@ function listRecentDirs(store, limit = 50) {
|
|
|
16650
17350
|
}
|
|
16651
17351
|
|
|
16652
17352
|
// src/version.ts
|
|
16653
|
-
var version = "0.1
|
|
17353
|
+
var version = "0.2.1";
|
|
16654
17354
|
|
|
16655
17355
|
// src/index.ts
|
|
16656
17356
|
async function startDaemon(config) {
|
|
16657
17357
|
const logger = createLogger({
|
|
16658
17358
|
level: config.logLevel,
|
|
16659
|
-
file:
|
|
17359
|
+
file: import_node_path15.default.join(config.dataDir, "clawd.log")
|
|
16660
17360
|
});
|
|
16661
17361
|
logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
|
|
16662
17362
|
const stateMgr = new StateFileManager({ dataDir: config.dataDir });
|
|
@@ -16667,9 +17367,20 @@ async function startDaemon(config) {
|
|
|
16667
17367
|
if (pre.status === "stale") {
|
|
16668
17368
|
logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
|
|
16669
17369
|
}
|
|
16670
|
-
|
|
16671
|
-
|
|
16672
|
-
|
|
17370
|
+
let resolvedAuthToken = null;
|
|
17371
|
+
if (config.authToken && config.authToken.trim()) {
|
|
17372
|
+
resolvedAuthToken = config.authToken.trim();
|
|
17373
|
+
} else if (config.tunnel) {
|
|
17374
|
+
resolvedAuthToken = loadOrCreateAuthToken({ dataDir: config.dataDir });
|
|
17375
|
+
}
|
|
17376
|
+
const authMode = resolvedAuthToken == null ? "none" : "first-message";
|
|
17377
|
+
let wsServer = null;
|
|
17378
|
+
const authGate = authMode === "first-message" ? new AuthGate({
|
|
17379
|
+
shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
|
|
17380
|
+
expectedToken: resolvedAuthToken,
|
|
17381
|
+
closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
|
|
17382
|
+
sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
|
|
17383
|
+
}) : null;
|
|
16673
17384
|
resetRegistry();
|
|
16674
17385
|
registerAdapter(
|
|
16675
17386
|
"claude",
|
|
@@ -16703,14 +17414,17 @@ async function startDaemon(config) {
|
|
|
16703
17414
|
onStatus: (sessionId, status) => transport?.broadcastToSession(sessionId, { type: "session:status", sessionId, status })
|
|
16704
17415
|
});
|
|
16705
17416
|
const handlers = buildMethodHandlers({ manager, workspace, skills, history, observer, getAdapter, store });
|
|
16706
|
-
|
|
17417
|
+
wsServer = new LocalWsServer({
|
|
16707
17418
|
host: config.host,
|
|
16708
17419
|
port: config.port,
|
|
16709
17420
|
logger,
|
|
16710
|
-
readyFrameBuilder: () => buildReadyFrame({ manager, getAdapter })
|
|
17421
|
+
readyFrameBuilder: () => buildReadyFrame({ manager, getAdapter }),
|
|
17422
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
17423
|
+
authGate: authGate ?? void 0
|
|
16711
17424
|
});
|
|
16712
17425
|
transport = wsServer;
|
|
16713
|
-
|
|
17426
|
+
const wss = wsServer;
|
|
17427
|
+
wss.onFrame(async (client, frame) => {
|
|
16714
17428
|
const type = frame.type;
|
|
16715
17429
|
if (!METHOD_NAMES.includes(type)) {
|
|
16716
17430
|
throw new ClawdError(ERROR_CODES.METHOD_NOT_ALLOWED, `unknown method: ${type}`);
|
|
@@ -16734,27 +17448,79 @@ async function startDaemon(config) {
|
|
|
16734
17448
|
}
|
|
16735
17449
|
}
|
|
16736
17450
|
});
|
|
16737
|
-
await
|
|
17451
|
+
await wss.start();
|
|
16738
17452
|
const url = `ws://${config.host}:${config.port}`;
|
|
16739
|
-
|
|
17453
|
+
let stateSnapshot = {
|
|
16740
17454
|
pid: process.pid,
|
|
16741
17455
|
transport: "local-ws",
|
|
16742
17456
|
url,
|
|
16743
17457
|
version,
|
|
16744
17458
|
protocolVersion: PROTOCOL_VERSION,
|
|
16745
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17459
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17460
|
+
authMode,
|
|
17461
|
+
authToken: resolvedAuthToken ?? void 0
|
|
16746
17462
|
};
|
|
16747
|
-
stateMgr.write(
|
|
17463
|
+
stateMgr.write(stateSnapshot);
|
|
16748
17464
|
process.stdout.write(`Ready: ${url}
|
|
16749
17465
|
`);
|
|
17466
|
+
if (authMode === "first-message" && resolvedAuthToken) {
|
|
17467
|
+
process.stdout.write(`Auth Token: ${resolvedAuthToken}
|
|
17468
|
+
`);
|
|
17469
|
+
}
|
|
17470
|
+
let tunnelMgr = null;
|
|
17471
|
+
if (config.tunnel) {
|
|
17472
|
+
if (!config.clawosApi) {
|
|
17473
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "--tunnel requires --clawos-api");
|
|
17474
|
+
}
|
|
17475
|
+
tunnelMgr = new TunnelManager({
|
|
17476
|
+
dataDir: config.dataDir,
|
|
17477
|
+
clawosApi: config.clawosApi,
|
|
17478
|
+
deviceKey: deriveStableDeviceKey(),
|
|
17479
|
+
frpcBinaryOverride: config.frpcBinary,
|
|
17480
|
+
noPersist: config.noTunnelPersist,
|
|
17481
|
+
logger,
|
|
17482
|
+
installProcessExitHandlers: true,
|
|
17483
|
+
onFrpcExit: (info) => {
|
|
17484
|
+
wss.broadcastAll({ type: "tunnel:exited", code: info.code, subdomain: info.subdomain, url: info.url });
|
|
17485
|
+
}
|
|
17486
|
+
});
|
|
17487
|
+
try {
|
|
17488
|
+
const r = await tunnelMgr.start({ localPort: config.port });
|
|
17489
|
+
stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url };
|
|
17490
|
+
stateMgr.write(stateSnapshot);
|
|
17491
|
+
process.stdout.write(`Tunnel: ${r.url}
|
|
17492
|
+
`);
|
|
17493
|
+
if (resolvedAuthToken) {
|
|
17494
|
+
process.stdout.write(`Connect: ${r.url}#token=${resolvedAuthToken}
|
|
17495
|
+
`);
|
|
17496
|
+
}
|
|
17497
|
+
} catch (err) {
|
|
17498
|
+
try {
|
|
17499
|
+
await tunnelMgr.stop();
|
|
17500
|
+
} catch {
|
|
17501
|
+
}
|
|
17502
|
+
tunnelMgr = null;
|
|
17503
|
+
stateMgr.delete();
|
|
17504
|
+
await wss.stop();
|
|
17505
|
+
throw err;
|
|
17506
|
+
}
|
|
17507
|
+
}
|
|
16750
17508
|
const shutdown = async () => {
|
|
16751
17509
|
logger.info("stopping clawd");
|
|
16752
17510
|
observer.stopAll();
|
|
16753
17511
|
manager.stopAll();
|
|
16754
|
-
|
|
17512
|
+
if (tunnelMgr) {
|
|
17513
|
+
await tunnelMgr.stop();
|
|
17514
|
+
}
|
|
17515
|
+
await wss.stop();
|
|
16755
17516
|
stateMgr.delete();
|
|
16756
17517
|
};
|
|
16757
|
-
return {
|
|
17518
|
+
return {
|
|
17519
|
+
stop: shutdown,
|
|
17520
|
+
url,
|
|
17521
|
+
tunnelUrl: stateSnapshot.tunnelUrl ?? null,
|
|
17522
|
+
authToken: resolvedAuthToken
|
|
17523
|
+
};
|
|
16758
17524
|
}
|
|
16759
17525
|
function buildMethodHandlers(deps) {
|
|
16760
17526
|
const { manager, workspace, skills, history, observer, getAdapter: getAdapter2, store } = deps;
|
|
@@ -16975,7 +17741,7 @@ function buildReadyFrame(deps) {
|
|
|
16975
17741
|
return {
|
|
16976
17742
|
version,
|
|
16977
17743
|
protocolVersion: PROTOCOL_VERSION,
|
|
16978
|
-
hostname:
|
|
17744
|
+
hostname: import_node_os10.default.hostname(),
|
|
16979
17745
|
os: process.platform,
|
|
16980
17746
|
tools,
|
|
16981
17747
|
runningSessions: info.runningSessions
|
|
@@ -16997,7 +17763,10 @@ async function main() {
|
|
|
16997
17763
|
}
|
|
16998
17764
|
const config = resolveConfig({ argv, env: process.env });
|
|
16999
17765
|
const runtime = await startDaemon(config);
|
|
17766
|
+
let shuttingDown = false;
|
|
17000
17767
|
const shutdown = async (signal) => {
|
|
17768
|
+
if (shuttingDown) return;
|
|
17769
|
+
shuttingDown = true;
|
|
17001
17770
|
try {
|
|
17002
17771
|
console.error(`[clawd] received ${signal}, shutting down`);
|
|
17003
17772
|
await runtime.stop();
|