@crabcloud/cli 0.1.8 → 0.1.10
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/dist/crab.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
2
16
|
|
|
3
17
|
// src/index.ts
|
|
4
18
|
import { hostname, homedir as homedir2 } from "node:os";
|
|
@@ -980,6 +994,7 @@ function datetime(args) {
|
|
|
980
994
|
return new RegExp(`^${dateSource}T(?:${timeRegex})$`);
|
|
981
995
|
}
|
|
982
996
|
var anyString = /^[\s\S]{0,}$/;
|
|
997
|
+
var bigint = /^-?\d+n?$/;
|
|
983
998
|
var integer = /^-?\d+$/;
|
|
984
999
|
var number = /^-?\d+(?:\.\d+)?$/;
|
|
985
1000
|
var boolean = /^(?:true|false)$/i;
|
|
@@ -1885,6 +1900,25 @@ var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
|
|
|
1885
1900
|
return payload;
|
|
1886
1901
|
};
|
|
1887
1902
|
});
|
|
1903
|
+
var $ZodBigInt = /* @__PURE__ */ $constructor("$ZodBigInt", (inst, def) => {
|
|
1904
|
+
$ZodType.init(inst, def);
|
|
1905
|
+
inst._zod.pattern = bigint;
|
|
1906
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
1907
|
+
if (def.coerce)
|
|
1908
|
+
try {
|
|
1909
|
+
payload.value = BigInt(payload.value);
|
|
1910
|
+
} catch (_) {}
|
|
1911
|
+
if (typeof payload.value === "bigint")
|
|
1912
|
+
return payload;
|
|
1913
|
+
payload.issues.push({
|
|
1914
|
+
expected: "bigint",
|
|
1915
|
+
code: "invalid_type",
|
|
1916
|
+
input: payload.value,
|
|
1917
|
+
inst
|
|
1918
|
+
});
|
|
1919
|
+
return payload;
|
|
1920
|
+
};
|
|
1921
|
+
});
|
|
1888
1922
|
var $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => {
|
|
1889
1923
|
$ZodType.init(inst, def);
|
|
1890
1924
|
inst._zod.pattern = _null;
|
|
@@ -1918,6 +1952,29 @@ var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => {
|
|
|
1918
1952
|
return payload;
|
|
1919
1953
|
};
|
|
1920
1954
|
});
|
|
1955
|
+
var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
|
|
1956
|
+
$ZodType.init(inst, def);
|
|
1957
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
1958
|
+
if (def.coerce) {
|
|
1959
|
+
try {
|
|
1960
|
+
payload.value = new Date(payload.value);
|
|
1961
|
+
} catch (_err) {}
|
|
1962
|
+
}
|
|
1963
|
+
const input = payload.value;
|
|
1964
|
+
const isDate = input instanceof Date;
|
|
1965
|
+
const isValidDate = isDate && !Number.isNaN(input.getTime());
|
|
1966
|
+
if (isValidDate)
|
|
1967
|
+
return payload;
|
|
1968
|
+
payload.issues.push({
|
|
1969
|
+
expected: "date",
|
|
1970
|
+
code: "invalid_type",
|
|
1971
|
+
input,
|
|
1972
|
+
...isDate ? { received: "Invalid Date" } : {},
|
|
1973
|
+
inst
|
|
1974
|
+
});
|
|
1975
|
+
return payload;
|
|
1976
|
+
};
|
|
1977
|
+
});
|
|
1921
1978
|
function handleArrayResult(result, final, index) {
|
|
1922
1979
|
if (result.issues.length) {
|
|
1923
1980
|
final.issues.push(...prefixIssues(index, result.issues));
|
|
@@ -3323,6 +3380,9 @@ function snapshotChecks(def) {
|
|
|
3323
3380
|
function _string(Class, params) {
|
|
3324
3381
|
return new Class(snapshotChecks({ type: "string", ...normalizeParams(params) }));
|
|
3325
3382
|
}
|
|
3383
|
+
function _coercedString(Class, params) {
|
|
3384
|
+
return new Class(snapshotChecks({ type: "string", coerce: true, ...normalizeParams(params) }));
|
|
3385
|
+
}
|
|
3326
3386
|
function _email(Class, params) {
|
|
3327
3387
|
return new Class({
|
|
3328
3388
|
type: "string",
|
|
@@ -3563,6 +3623,9 @@ function _isoDuration(Class, params) {
|
|
|
3563
3623
|
function _number(Class, params) {
|
|
3564
3624
|
return new Class(snapshotChecks({ type: "number", checks: [], ...normalizeParams(params) }));
|
|
3565
3625
|
}
|
|
3626
|
+
function _coercedNumber(Class, params) {
|
|
3627
|
+
return new Class(snapshotChecks({ type: "number", coerce: true, checks: [], ...normalizeParams(params) }));
|
|
3628
|
+
}
|
|
3566
3629
|
function _int(Class, params) {
|
|
3567
3630
|
return new Class({
|
|
3568
3631
|
type: "number",
|
|
@@ -3578,6 +3641,20 @@ function _boolean(Class, params) {
|
|
|
3578
3641
|
...normalizeParams(params)
|
|
3579
3642
|
});
|
|
3580
3643
|
}
|
|
3644
|
+
function _coercedBoolean(Class, params) {
|
|
3645
|
+
return new Class({
|
|
3646
|
+
type: "boolean",
|
|
3647
|
+
coerce: true,
|
|
3648
|
+
...normalizeParams(params)
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
function _coercedBigint(Class, params) {
|
|
3652
|
+
return new Class({
|
|
3653
|
+
type: "bigint",
|
|
3654
|
+
coerce: true,
|
|
3655
|
+
...normalizeParams(params)
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3581
3658
|
function _null2(Class, params) {
|
|
3582
3659
|
return new Class({
|
|
3583
3660
|
type: "null",
|
|
@@ -3595,6 +3672,13 @@ function _never(Class, params) {
|
|
|
3595
3672
|
...normalizeParams(params)
|
|
3596
3673
|
});
|
|
3597
3674
|
}
|
|
3675
|
+
function _coercedDate(Class, params) {
|
|
3676
|
+
return new Class({
|
|
3677
|
+
type: "date",
|
|
3678
|
+
coerce: true,
|
|
3679
|
+
...normalizeParams(params)
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3598
3682
|
function _lt(value, params) {
|
|
3599
3683
|
return new $ZodCheckLessThan({
|
|
3600
3684
|
check: "less_than",
|
|
@@ -4469,6 +4553,9 @@ var numberProcessor = (schema, ctx, _json, params) => {
|
|
|
4469
4553
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4470
4554
|
json.type = "boolean";
|
|
4471
4555
|
};
|
|
4556
|
+
var bigintProcessor = (schema, ctx, json, params) => {
|
|
4557
|
+
handleUnrepresentable(schema, ctx, json, params, "BigInt cannot be represented in JSON Schema");
|
|
4558
|
+
};
|
|
4472
4559
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
4473
4560
|
if (ctx.target === "openapi-3.0") {
|
|
4474
4561
|
json.type = "string";
|
|
@@ -4482,6 +4569,9 @@ var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
|
4482
4569
|
json.not = {};
|
|
4483
4570
|
};
|
|
4484
4571
|
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4572
|
+
var dateProcessor = (schema, ctx, json, params) => {
|
|
4573
|
+
handleUnrepresentable(schema, ctx, json, params, "Date cannot be represented in JSON Schema");
|
|
4574
|
+
};
|
|
4485
4575
|
var enumProcessor = (schema, _ctx, json, _params) => {
|
|
4486
4576
|
const def = schema._zod.def;
|
|
4487
4577
|
const values = getEnumValues(def.entries);
|
|
@@ -5346,6 +5436,49 @@ var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
|
|
|
5346
5436
|
function boolean2(params) {
|
|
5347
5437
|
return _boolean(ZodBoolean, params);
|
|
5348
5438
|
}
|
|
5439
|
+
var ZodBigInt = /* @__PURE__ */ $constructor("ZodBigInt", (inst, def) => {
|
|
5440
|
+
$ZodBigInt.init(inst, def);
|
|
5441
|
+
ZodType.init(inst, def);
|
|
5442
|
+
inst._zod.processJSONSchema = (ctx, json, params) => bigintProcessor(inst, ctx, json, params);
|
|
5443
|
+
}, /* @__PURE__ */ derived({
|
|
5444
|
+
minValue: (inst) => aggregateChecks(inst).minimum ?? null,
|
|
5445
|
+
maxValue: (inst) => aggregateChecks(inst).maximum ?? null,
|
|
5446
|
+
format: (inst) => aggregateChecks(inst).format ?? null
|
|
5447
|
+
}, {
|
|
5448
|
+
gte(value, params) {
|
|
5449
|
+
return this.check(_gte(value, params));
|
|
5450
|
+
},
|
|
5451
|
+
min(value, params) {
|
|
5452
|
+
return this.check(_gte(value, params));
|
|
5453
|
+
},
|
|
5454
|
+
gt(value, params) {
|
|
5455
|
+
return this.check(_gt(value, params));
|
|
5456
|
+
},
|
|
5457
|
+
lt(value, params) {
|
|
5458
|
+
return this.check(_lt(value, params));
|
|
5459
|
+
},
|
|
5460
|
+
lte(value, params) {
|
|
5461
|
+
return this.check(_lte(value, params));
|
|
5462
|
+
},
|
|
5463
|
+
max(value, params) {
|
|
5464
|
+
return this.check(_lte(value, params));
|
|
5465
|
+
},
|
|
5466
|
+
positive(params) {
|
|
5467
|
+
return this.check(_gt(BigInt(0), params));
|
|
5468
|
+
},
|
|
5469
|
+
negative(params) {
|
|
5470
|
+
return this.check(_lt(BigInt(0), params));
|
|
5471
|
+
},
|
|
5472
|
+
nonpositive(params) {
|
|
5473
|
+
return this.check(_lte(BigInt(0), params));
|
|
5474
|
+
},
|
|
5475
|
+
nonnegative(params) {
|
|
5476
|
+
return this.check(_gte(BigInt(0), params));
|
|
5477
|
+
},
|
|
5478
|
+
multipleOf(value, params) {
|
|
5479
|
+
return this.check(_multipleOf(value, params));
|
|
5480
|
+
}
|
|
5481
|
+
}));
|
|
5349
5482
|
var ZodNull = /* @__PURE__ */ $constructor("ZodNull", (inst, def) => {
|
|
5350
5483
|
$ZodNull.init(inst, def);
|
|
5351
5484
|
ZodType.init(inst, def);
|
|
@@ -5370,6 +5503,22 @@ var ZodNever = /* @__PURE__ */ $constructor("ZodNever", (inst, def) => {
|
|
|
5370
5503
|
function never(params) {
|
|
5371
5504
|
return _never(ZodNever, params);
|
|
5372
5505
|
}
|
|
5506
|
+
var ZodDate = /* @__PURE__ */ $constructor("ZodDate", (inst, def) => {
|
|
5507
|
+
$ZodDate.init(inst, def);
|
|
5508
|
+
ZodType.init(inst, def);
|
|
5509
|
+
inst._zod.processJSONSchema = (ctx, json, params) => dateProcessor(inst, ctx, json, params);
|
|
5510
|
+
inst.min = (value, params) => inst.check(_gte(value, params));
|
|
5511
|
+
inst.max = (value, params) => inst.check(_lte(value, params));
|
|
5512
|
+
}, /* @__PURE__ */ derived({
|
|
5513
|
+
minDate: (inst) => {
|
|
5514
|
+
const { minimum } = aggregateChecks(inst);
|
|
5515
|
+
return minimum ? new Date(minimum) : null;
|
|
5516
|
+
},
|
|
5517
|
+
maxDate: (inst) => {
|
|
5518
|
+
const { maximum } = aggregateChecks(inst);
|
|
5519
|
+
return maximum ? new Date(maximum) : null;
|
|
5520
|
+
}
|
|
5521
|
+
}, {}));
|
|
5373
5522
|
var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
|
|
5374
5523
|
_ensureDefaultMemoizer();
|
|
5375
5524
|
$ZodArray.init(inst, def);
|
|
@@ -5720,6 +5869,30 @@ function refine(fn, _params = {}) {
|
|
|
5720
5869
|
function superRefine(fn, params) {
|
|
5721
5870
|
return _superRefine(fn, params);
|
|
5722
5871
|
}
|
|
5872
|
+
// ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/classic/coerce.js
|
|
5873
|
+
var exports_coerce = {};
|
|
5874
|
+
__export(exports_coerce, {
|
|
5875
|
+
bigint: () => bigint2,
|
|
5876
|
+
boolean: () => boolean3,
|
|
5877
|
+
date: () => date2,
|
|
5878
|
+
number: () => number3,
|
|
5879
|
+
string: () => string3
|
|
5880
|
+
});
|
|
5881
|
+
function string3(params) {
|
|
5882
|
+
return _coercedString(ZodString, params);
|
|
5883
|
+
}
|
|
5884
|
+
function number3(params) {
|
|
5885
|
+
return _coercedNumber(ZodNumber, params);
|
|
5886
|
+
}
|
|
5887
|
+
function boolean3(params) {
|
|
5888
|
+
return _coercedBoolean(ZodBoolean, params);
|
|
5889
|
+
}
|
|
5890
|
+
function bigint2(params) {
|
|
5891
|
+
return _coercedBigint(ZodBigInt, params);
|
|
5892
|
+
}
|
|
5893
|
+
function date2(params) {
|
|
5894
|
+
return _coercedDate(ZodDate, params);
|
|
5895
|
+
}
|
|
5723
5896
|
// ../../packages/contracts/src/index.ts
|
|
5724
5897
|
var accountDomain = "crabcloud.cc";
|
|
5725
5898
|
var adminDomain = `admin.${accountDomain}`;
|
|
@@ -5773,6 +5946,8 @@ var apiErrorSchema = object({
|
|
|
5773
5946
|
"expired",
|
|
5774
5947
|
"insufficient_credits",
|
|
5775
5948
|
"payload_too_large",
|
|
5949
|
+
"collab_space_not_bound",
|
|
5950
|
+
"collab_space_mismatch",
|
|
5776
5951
|
"internal"
|
|
5777
5952
|
]),
|
|
5778
5953
|
message: string2()
|
|
@@ -5827,7 +6002,8 @@ var sessionSummarySchema = object({
|
|
|
5827
6002
|
});
|
|
5828
6003
|
var deviceStartInput = object({
|
|
5829
6004
|
name: string2().trim().min(1).max(80),
|
|
5830
|
-
scopes: array(_enum(scopes)).min(1).max(scopes.length)
|
|
6005
|
+
scopes: array(_enum(scopes)).min(1).max(scopes.length),
|
|
6006
|
+
space: string2().trim().min(1).max(80).optional()
|
|
5831
6007
|
});
|
|
5832
6008
|
var devicePollInput = object({ deviceCode: string2().min(16).max(200) });
|
|
5833
6009
|
var deviceDecisionInput = object({
|
|
@@ -5839,6 +6015,7 @@ var agentTokenSummarySchema = object({
|
|
|
5839
6015
|
id: string2(),
|
|
5840
6016
|
name: string2(),
|
|
5841
6017
|
scopes: array(_enum(scopes)),
|
|
6018
|
+
collabSpace: string2().nullable().optional(),
|
|
5842
6019
|
createdAt: number2(),
|
|
5843
6020
|
lastUsedAt: number2().nullable(),
|
|
5844
6021
|
expiresAt: number2().nullable(),
|
|
@@ -5869,12 +6046,30 @@ var adminCreditPackInput = object({
|
|
|
5869
6046
|
title: string2().trim().min(1).max(100),
|
|
5870
6047
|
credits: number2().int().positive().max(1e7),
|
|
5871
6048
|
priceCents: number2().int().positive(),
|
|
5872
|
-
currency: string2().trim().length(3).default("
|
|
6049
|
+
currency: string2().trim().length(3).default("USD"),
|
|
5873
6050
|
sort: number2().int().min(0).max(1e4).default(0)
|
|
5874
6051
|
});
|
|
5875
6052
|
var adminCreditPackUpdateInput = adminCreditPackInput.partial().extend({
|
|
5876
6053
|
status: _enum(creditPackStatuses).optional()
|
|
5877
6054
|
});
|
|
6055
|
+
var adminPricingOverridesInput = object({
|
|
6056
|
+
overrides: record(string2().trim().min(1).max(100), union([number2().int().positive().max(1e8), _null3()]))
|
|
6057
|
+
});
|
|
6058
|
+
var adminPlanTierStatuses = ["active", "hidden"];
|
|
6059
|
+
var adminPlanTierUpdateInput = object({
|
|
6060
|
+
name: string2().trim().min(1).max(50).optional(),
|
|
6061
|
+
priceCents: number2().int().min(0).max(1e8).nullable().optional(),
|
|
6062
|
+
monthlyCredits: number2().int().min(0).max(1e8).optional(),
|
|
6063
|
+
quotaJson: record(string2(), unknown()).optional(),
|
|
6064
|
+
status: _enum(adminPlanTierStatuses).optional()
|
|
6065
|
+
});
|
|
6066
|
+
var subscriptionChangeInput = object({
|
|
6067
|
+
tierId: string2().trim().min(1).max(50)
|
|
6068
|
+
});
|
|
6069
|
+
var adminUserSubscriptionInput = object({
|
|
6070
|
+
tierId: string2().trim().min(1).max(50),
|
|
6071
|
+
reason: string2().trim().min(1).max(500)
|
|
6072
|
+
});
|
|
5878
6073
|
var giftFulfillments = ["stock", "webhook"];
|
|
5879
6074
|
var giftMerchantStatuses = ["active", "disabled"];
|
|
5880
6075
|
var giftProductStatuses = ["draft", "live", "delisted"];
|
|
@@ -5979,6 +6174,100 @@ var contactQueryInput = object({
|
|
|
5979
6174
|
limit: number2().int().min(1).max(50).default(20),
|
|
5980
6175
|
offset: number2().int().min(0).default(0)
|
|
5981
6176
|
});
|
|
6177
|
+
var collabTaskStatuses = ["open", "done"];
|
|
6178
|
+
var collabBoostKinds = ["tasks", "members"];
|
|
6179
|
+
var COLLAB_RECORD_BODY_MAX = 20000;
|
|
6180
|
+
var COLLAB_RECORD_FILES_MAX = 10;
|
|
6181
|
+
var COLLAB_RECORD_LINKS_MAX = 10;
|
|
6182
|
+
var COLLAB_TITLE_MAX = 200;
|
|
6183
|
+
var COLLAB_SPACE_NAME_MAX = 64;
|
|
6184
|
+
var COLLAB_SUMMARY_MAX = 2000;
|
|
6185
|
+
var collabCreateSpaceInput = object({
|
|
6186
|
+
name: string2().trim().min(1).max(COLLAB_SPACE_NAME_MAX),
|
|
6187
|
+
allocationBytes: number2().int().min(1).max(10 * 1024 * 1024 * 1024).optional()
|
|
6188
|
+
});
|
|
6189
|
+
var collabRenameSpaceInput = object({ name: string2().trim().min(1).max(COLLAB_SPACE_NAME_MAX) });
|
|
6190
|
+
var collabAllocationInput = object({
|
|
6191
|
+
allocationBytes: number2().int().min(1).max(10 * 1024 * 1024 * 1024)
|
|
6192
|
+
});
|
|
6193
|
+
var collabBoostInput = object({ kind: _enum(collabBoostKinds) });
|
|
6194
|
+
var collabInviteInput = object({ username: usernameSchema });
|
|
6195
|
+
var collabLinkInput = string2().trim().url().max(2000);
|
|
6196
|
+
var collabCreateTaskInput = object({
|
|
6197
|
+
title: string2().trim().min(1).max(COLLAB_TITLE_MAX),
|
|
6198
|
+
body: string2().max(COLLAB_RECORD_BODY_MAX).default(""),
|
|
6199
|
+
fileIds: array(string2().trim().min(1).max(64)).max(COLLAB_RECORD_FILES_MAX).optional(),
|
|
6200
|
+
links: array(collabLinkInput).max(COLLAB_RECORD_LINKS_MAX).optional()
|
|
6201
|
+
});
|
|
6202
|
+
var collabPushRecordInput = object({
|
|
6203
|
+
body: string2().min(1).max(COLLAB_RECORD_BODY_MAX),
|
|
6204
|
+
fileIds: array(string2().trim().min(1).max(64)).max(COLLAB_RECORD_FILES_MAX).optional(),
|
|
6205
|
+
links: array(collabLinkInput).max(COLLAB_RECORD_LINKS_MAX).optional()
|
|
6206
|
+
});
|
|
6207
|
+
var collabUpdateSummaryInput = object({
|
|
6208
|
+
summary: string2().trim().max(COLLAB_SUMMARY_MAX)
|
|
6209
|
+
});
|
|
6210
|
+
var collabTaskListQuery = object({
|
|
6211
|
+
status: _enum(collabTaskStatuses).optional(),
|
|
6212
|
+
q: string2().trim().max(200).optional(),
|
|
6213
|
+
cursor: string2().trim().max(200).optional(),
|
|
6214
|
+
limit: exports_coerce.number().int().min(1).max(100).optional()
|
|
6215
|
+
});
|
|
6216
|
+
var collabRecordListQuery = object({
|
|
6217
|
+
cursor: string2().trim().max(200).optional(),
|
|
6218
|
+
limit: exports_coerce.number().int().min(1).max(200).optional()
|
|
6219
|
+
});
|
|
6220
|
+
|
|
6221
|
+
// ../../packages/cloudkit/src/crypto.ts
|
|
6222
|
+
var enc = new TextEncoder;
|
|
6223
|
+
var b64urlEncode = (bytes) => {
|
|
6224
|
+
let s = "";
|
|
6225
|
+
for (const b of bytes)
|
|
6226
|
+
s += String.fromCharCode(b);
|
|
6227
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6228
|
+
};
|
|
6229
|
+
var b64urlDecode = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
6230
|
+
function randomCode(length = 6) {
|
|
6231
|
+
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
6232
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(length)), (byte) => alphabet[byte % alphabet.length]).join("");
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
// ../../packages/cloudkit/src/secretbox.ts
|
|
6236
|
+
var enc2 = new TextEncoder;
|
|
6237
|
+
var utf8 = (s) => enc2.encode(s);
|
|
6238
|
+
var DEFAULT_KEK_ITERATIONS = 210000;
|
|
6239
|
+
async function deriveKekBits(password, salt, iterations = DEFAULT_KEK_ITERATIONS) {
|
|
6240
|
+
const key = await crypto.subtle.importKey("raw", utf8(password), "PBKDF2", false, ["deriveBits"]);
|
|
6241
|
+
return new Uint8Array(await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations }, key, 256));
|
|
6242
|
+
}
|
|
6243
|
+
async function importAesKey(raw, usages = ["encrypt", "decrypt"]) {
|
|
6244
|
+
return crypto.subtle.importKey("raw", raw, "AES-GCM", false, usages);
|
|
6245
|
+
}
|
|
6246
|
+
function randomKeyBytes(length = 32) {
|
|
6247
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
6248
|
+
}
|
|
6249
|
+
async function aeadEncrypt(key, plaintext, aad) {
|
|
6250
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
6251
|
+
const params = aad ? { name: "AES-GCM", iv, additionalData: aad } : { name: "AES-GCM", iv };
|
|
6252
|
+
const ciphertext = await crypto.subtle.encrypt(params, key, plaintext);
|
|
6253
|
+
return { iv, ciphertext: new Uint8Array(ciphertext) };
|
|
6254
|
+
}
|
|
6255
|
+
async function aeadDecrypt(key, sealed, aad) {
|
|
6256
|
+
const params = aad ? { name: "AES-GCM", iv: sealed.iv, additionalData: aad } : { name: "AES-GCM", iv: sealed.iv };
|
|
6257
|
+
return new Uint8Array(await crypto.subtle.decrypt(params, key, sealed.ciphertext));
|
|
6258
|
+
}
|
|
6259
|
+
async function unwrapKeyBytes(kek, sealed, aad) {
|
|
6260
|
+
return aeadDecrypt(kek, sealed, aad);
|
|
6261
|
+
}
|
|
6262
|
+
function sealToB64url(sealed) {
|
|
6263
|
+
return `${b64urlEncode(sealed.iv)}.${b64urlEncode(sealed.ciphertext)}`;
|
|
6264
|
+
}
|
|
6265
|
+
function sealFromB64url(s) {
|
|
6266
|
+
const [iv, ct] = s.split(".");
|
|
6267
|
+
if (!iv || !ct)
|
|
6268
|
+
throw new Error("invalid sealed payload");
|
|
6269
|
+
return { iv: b64urlDecode(iv), ciphertext: b64urlDecode(ct) };
|
|
6270
|
+
}
|
|
5982
6271
|
|
|
5983
6272
|
// ../../packages/skills/src/install.ts
|
|
5984
6273
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -6247,6 +6536,9 @@ function requireCredentials() {
|
|
|
6247
6536
|
function formatTime(ms) {
|
|
6248
6537
|
return new Date(ms).toLocaleString("sv", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
6249
6538
|
}
|
|
6539
|
+
function fmtCredits(cents) {
|
|
6540
|
+
return cents.toLocaleString("en-US");
|
|
6541
|
+
}
|
|
6250
6542
|
var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
6251
6543
|
发布物经 npm 分发:npx @crabcloud/cli <command>(全局安装后短名 crab 等价)
|
|
6252
6544
|
|
|
@@ -6268,9 +6560,14 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6268
6560
|
列出邮件(cursor 分页契约与 Web 工作台一致)
|
|
6269
6561
|
mail read <id> [--json] 读取邮件正文(自动置已读)
|
|
6270
6562
|
mail search <query> [--json] 全文搜索(主题/发件人/收件人/正文)
|
|
6271
|
-
mail send --to a@x.cc
|
|
6272
|
-
[--in-reply-to <message-id>]
|
|
6273
|
-
|
|
6563
|
+
mail send --to a@x.cc|联系人名,b@y.cn [--cc c@z.io] --subject "..." --body "..." | --body-file <path>
|
|
6564
|
+
[--attach <云盘id|文件名>,...] [--in-reply-to <message-id>]
|
|
6565
|
+
回信线程锚点(原信 RFC Message-ID,crab mail read 可见)
|
|
6566
|
+
发送邮件:不含 @ 的收件人按联系人簿解析(备注名/地址精确
|
|
6567
|
+
命中采用,多候选报错列出);--attach 引用云盘已有对象
|
|
6568
|
+
(id 或文件名精确唯一命中,未绑信对象,≤10 个,大文件
|
|
6569
|
+
自动转下载链接);平台内(@crabcloud.cc)免费,外部出站
|
|
6570
|
+
2 积分/封
|
|
6274
6571
|
mail archive|trash|delete <id> 移夹 / 废纸篓 / 彻底删除(delete 不可恢复)
|
|
6275
6572
|
contacts list [query] [--offset N] [--json] 列出联系人(地址前缀/显示名匹配)
|
|
6276
6573
|
contacts add <address> [--name 名] [--note 备注] [--json]
|
|
@@ -6281,7 +6578,7 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6281
6578
|
我邀请的人(直接下级 + 间接计数)
|
|
6282
6579
|
storage ls [--q 关键字] [--source files|media|mail] [--category <id|none>]
|
|
6283
6580
|
[--type doc|image|video|archive|code] [--tag a,b] [--trash]
|
|
6284
|
-
[--shared] [--recent] [--limit N] [--offset N] [--json]
|
|
6581
|
+
[--shared] [--recent] [--message-id <id>] [--limit N] [--offset N] [--json]
|
|
6285
6582
|
列出云盘对象(跨来源统一视图,能力与 Web 云盘一致)
|
|
6286
6583
|
storage upload <file...> [--json] 上传一个或多个文件(直传优先,自动回退中转)
|
|
6287
6584
|
storage download <id|文件名> [-o 输出路径]
|
|
@@ -6296,9 +6593,18 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6296
6593
|
storage rm|restore|purge <id|文件名>
|
|
6297
6594
|
回收站 / 恢复 / 彻底删除(purge 不可恢复)
|
|
6298
6595
|
storage quota [--json] 存储水位(统一容量账:文件/素材/邮件附件一本账)
|
|
6596
|
+
storage share <id|文件名> [--expires 7|30|90|forever] [--passcode 提取码]
|
|
6597
|
+
创建分享链接(加密文件链接自带解密钥匙 #fragment)
|
|
6598
|
+
storage shares <id|文件名> [--json]
|
|
6599
|
+
该文件的分享链接列表(状态/下载计数)
|
|
6600
|
+
storage unshare <shareId> 撤销分享
|
|
6299
6601
|
storage categories [--json] 分类列表
|
|
6300
6602
|
storage mkcat <名称> 新建分类
|
|
6301
6603
|
storage tags [--json] 标签汇总
|
|
6604
|
+
credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
|
|
6605
|
+
credits history [--limit N] [--offset N] [--json]
|
|
6606
|
+
积分流水(变动 ±:消耗为负;需 credits.read)
|
|
6607
|
+
credits pricing [--json] 动作计价标准(公开:动作单价/档位/充值包)
|
|
6302
6608
|
skills install|list|uninstall [--agent claude,zcode,codex,agents]
|
|
6303
6609
|
skills 管理(claude/zcode/agents 复制目录,codex 写 AGENTS.md 段)
|
|
6304
6610
|
logout 撤销当前 Agent Token 并清除本地凭证
|
|
@@ -6401,11 +6707,105 @@ async function whoami() {
|
|
|
6401
6707
|
if (subscription)
|
|
6402
6708
|
console.log(` 订阅: ${subscription.tier_name}`);
|
|
6403
6709
|
if (credits !== undefined)
|
|
6404
|
-
console.log(` 积分余额: ${credits}
|
|
6710
|
+
console.log(` 积分余额: ${fmtCredits(credits)} 积分`);
|
|
6405
6711
|
if (apps?.length)
|
|
6406
6712
|
console.log(` 已启用应用: ${apps.map((a) => a.title).join("、")}`);
|
|
6407
6713
|
return 0;
|
|
6408
6714
|
}
|
|
6715
|
+
var CREDIT_REASON_LABELS = {
|
|
6716
|
+
subscription_grant: "月送发放",
|
|
6717
|
+
topup: "充值入账",
|
|
6718
|
+
consume: "动作消耗",
|
|
6719
|
+
refund: "退款",
|
|
6720
|
+
adjustment: "人工调整"
|
|
6721
|
+
};
|
|
6722
|
+
var CREDIT_ACTION_LABELS = {
|
|
6723
|
+
"mail.send_outbound": "外部出站邮件",
|
|
6724
|
+
"collab.space_create": "创建独立协作空间",
|
|
6725
|
+
"collab.boost_tasks": "协作任务扩容(+20)",
|
|
6726
|
+
"collab.boost_members": "协作成员扩容(+5)"
|
|
6727
|
+
};
|
|
6728
|
+
function creditMovement(entry) {
|
|
6729
|
+
if (entry.grantedDelta === null && entry.topupDelta === null)
|
|
6730
|
+
return entry.delta;
|
|
6731
|
+
return (entry.grantedDelta ?? 0) + (entry.topupDelta ?? 0);
|
|
6732
|
+
}
|
|
6733
|
+
async function fetchCredits(credentials, limit, offset) {
|
|
6734
|
+
const res = await api2(credentials.apiBase, `/v1/me/credits?limit=${limit}&offset=${offset}`, { headers: authHeaders(credentials) });
|
|
6735
|
+
if (res.status !== 200) {
|
|
6736
|
+
console.error(errorMessage(res.body, "获取积分数据失败。"));
|
|
6737
|
+
if (res.body?.error?.code === "scope_denied") {
|
|
6738
|
+
console.error("提示:积分命令需要 credits.read scope——运行 crab login --scopes account 重新授权。");
|
|
6739
|
+
}
|
|
6740
|
+
return exitCodeForApiError(res.body);
|
|
6741
|
+
}
|
|
6742
|
+
return res.body.data;
|
|
6743
|
+
}
|
|
6744
|
+
async function creditsBalance() {
|
|
6745
|
+
const credentials = requireCredentials();
|
|
6746
|
+
if (typeof credentials === "number")
|
|
6747
|
+
return credentials;
|
|
6748
|
+
const data = await fetchCredits(credentials, 1, 0);
|
|
6749
|
+
if (typeof data === "number")
|
|
6750
|
+
return data;
|
|
6751
|
+
if (hasFlag("--json")) {
|
|
6752
|
+
console.log(JSON.stringify(data.balance, null, 2));
|
|
6753
|
+
return 0;
|
|
6754
|
+
}
|
|
6755
|
+
console.log(`积分余额:${fmtCredits(data.balance.balance)}`);
|
|
6756
|
+
console.log(` 月送 ${fmtCredits(data.balance.granted)}(每周期刷新清零) · 充值 ${fmtCredits(data.balance.topup)}(长期有效)`);
|
|
6757
|
+
return 0;
|
|
6758
|
+
}
|
|
6759
|
+
async function creditsHistory() {
|
|
6760
|
+
const credentials = requireCredentials();
|
|
6761
|
+
if (typeof credentials === "number")
|
|
6762
|
+
return credentials;
|
|
6763
|
+
const limit = Math.min(Math.max(Number.parseInt(readFlag("--limit") ?? "", 10) || 20, 1), 100);
|
|
6764
|
+
const offset = Math.max(Number.parseInt(readFlag("--offset") ?? "", 10) || 0, 0);
|
|
6765
|
+
const data = await fetchCredits(credentials, limit, offset);
|
|
6766
|
+
if (typeof data === "number")
|
|
6767
|
+
return data;
|
|
6768
|
+
if (hasFlag("--json")) {
|
|
6769
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6770
|
+
return 0;
|
|
6771
|
+
}
|
|
6772
|
+
if (data.ledger.length === 0) {
|
|
6773
|
+
console.log("还没有积分流水。");
|
|
6774
|
+
return 0;
|
|
6775
|
+
}
|
|
6776
|
+
for (const entry of data.ledger) {
|
|
6777
|
+
const movement = creditMovement(entry);
|
|
6778
|
+
const signed = movement > 0 ? `+${fmtCredits(movement)}` : fmtCredits(movement);
|
|
6779
|
+
console.log(`${formatTime(entry.createdAt)} ${CREDIT_REASON_LABELS[entry.reason] ?? entry.reason} ${signed}`);
|
|
6780
|
+
}
|
|
6781
|
+
console.log(`
|
|
6782
|
+
共 ${fmtCredits(data.total)} 条(本页 ${data.ledger.length} 条,--offset 翻页)`);
|
|
6783
|
+
return 0;
|
|
6784
|
+
}
|
|
6785
|
+
async function creditsPricing() {
|
|
6786
|
+
const credentials = loadCredentials();
|
|
6787
|
+
const apiBase = credentials ? credentials.apiBase : process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
|
|
6788
|
+
const res = await api2(apiBase, "/v1/pricing/standard");
|
|
6789
|
+
if (res.status !== 200) {
|
|
6790
|
+
console.error(errorMessage(res.body, "获取动作计价失败。"));
|
|
6791
|
+
return exitCodeForApiError(res.body);
|
|
6792
|
+
}
|
|
6793
|
+
const actions = res.body.data?.actions ?? [];
|
|
6794
|
+
if (hasFlag("--json")) {
|
|
6795
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
6796
|
+
return 0;
|
|
6797
|
+
}
|
|
6798
|
+
if (actions.length === 0) {
|
|
6799
|
+
console.log("暂无动作计价项。");
|
|
6800
|
+
return 0;
|
|
6801
|
+
}
|
|
6802
|
+
console.log("动作计价(credit_cents,100 = $1.00):");
|
|
6803
|
+
for (const item of actions) {
|
|
6804
|
+
const label = CREDIT_ACTION_LABELS[item.action] ?? item.action;
|
|
6805
|
+
console.log(` ${item.action.padEnd(24)} ${label} ${fmtCredits(item.creditCents)} 积分`);
|
|
6806
|
+
}
|
|
6807
|
+
return 0;
|
|
6808
|
+
}
|
|
6409
6809
|
async function tokensList() {
|
|
6410
6810
|
const credentials = requireCredentials();
|
|
6411
6811
|
if (typeof credentials === "number")
|
|
@@ -6530,6 +6930,13 @@ async function mailRead(id) {
|
|
|
6530
6930
|
console.log(` 回复: ${mail.inReplyTo}`);
|
|
6531
6931
|
console.log("");
|
|
6532
6932
|
console.log(mail.bodyText);
|
|
6933
|
+
if (mail.attachments.length > 0) {
|
|
6934
|
+
console.log("");
|
|
6935
|
+
console.log("附件:");
|
|
6936
|
+
for (const att of mail.attachments) {
|
|
6937
|
+
console.log(` ${att.id} ${att.filename} ${att.mimeType} ${formatBytesCli(att.sizeBytes)}`);
|
|
6938
|
+
}
|
|
6939
|
+
}
|
|
6533
6940
|
if (mail.actors.length > 0) {
|
|
6534
6941
|
console.log("");
|
|
6535
6942
|
console.log("操作记录:");
|
|
@@ -6565,15 +6972,63 @@ async function mailSearch(query) {
|
|
|
6565
6972
|
printMailRow(mail);
|
|
6566
6973
|
return 0;
|
|
6567
6974
|
}
|
|
6975
|
+
async function resolveRecipients(credentials, tokens) {
|
|
6976
|
+
const resolved = [];
|
|
6977
|
+
for (const token of tokens) {
|
|
6978
|
+
if (token.includes("@")) {
|
|
6979
|
+
resolved.push(token);
|
|
6980
|
+
continue;
|
|
6981
|
+
}
|
|
6982
|
+
const params = new URLSearchParams({ query: token, limit: "50" });
|
|
6983
|
+
const res = await api2(credentials.apiBase, `/v1/contacts?${params}`, { headers: authHeaders(credentials) });
|
|
6984
|
+
if (res.status !== 200) {
|
|
6985
|
+
console.error(errorMessage(res.body, "查询联系人失败。"));
|
|
6986
|
+
return null;
|
|
6987
|
+
}
|
|
6988
|
+
const items = res.body.data.items;
|
|
6989
|
+
const lowered = token.toLowerCase();
|
|
6990
|
+
const exact = items.filter((c) => c.address.toLowerCase() === lowered || (c.displayName ?? "").toLowerCase() === lowered);
|
|
6991
|
+
if (exact.length === 1) {
|
|
6992
|
+
resolved.push(exact[0].address);
|
|
6993
|
+
continue;
|
|
6994
|
+
}
|
|
6995
|
+
if (exact.length > 1) {
|
|
6996
|
+
console.error(`「${token}」匹配到多个联系人,请改用完整地址:`);
|
|
6997
|
+
for (const c of exact.slice(0, 5))
|
|
6998
|
+
console.error(` ${contactDisplayName(c)} <${c.address}>`);
|
|
6999
|
+
return null;
|
|
7000
|
+
}
|
|
7001
|
+
if (items.length === 1) {
|
|
7002
|
+
console.error(`「${token}」→ ${contactDisplayName(items[0])} <${items[0].address}>`);
|
|
7003
|
+
resolved.push(items[0].address);
|
|
7004
|
+
continue;
|
|
7005
|
+
}
|
|
7006
|
+
console.error(`「${token}」没有唯一命中的联系人。`);
|
|
7007
|
+
if (items.length > 0) {
|
|
7008
|
+
console.error("相近候选:");
|
|
7009
|
+
for (const c of items.slice(0, 5))
|
|
7010
|
+
console.error(` ${contactDisplayName(c)} <${c.address}>`);
|
|
7011
|
+
} else {
|
|
7012
|
+
console.error("联系人簿没有相近条目(可先 crab contacts list 查看)。");
|
|
7013
|
+
}
|
|
7014
|
+
return null;
|
|
7015
|
+
}
|
|
7016
|
+
return resolved;
|
|
7017
|
+
}
|
|
6568
7018
|
async function mailSend() {
|
|
6569
|
-
const
|
|
6570
|
-
const
|
|
7019
|
+
const toRaw = (readFlag("--to") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
7020
|
+
const ccRaw = (readFlag("--cc") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
7021
|
+
const attachRaw = (readFlag("--attach") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
6571
7022
|
const subject = readFlag("--subject") ?? "";
|
|
6572
7023
|
const bodyFlag = readFlag("--body");
|
|
6573
7024
|
const bodyFile = readFlag("--body-file");
|
|
6574
7025
|
const inReplyTo = readFlag("--in-reply-to") ?? undefined;
|
|
6575
|
-
if (
|
|
6576
|
-
console.error('用法:crab mail send --to a@x.cc[,b@y.cn] [--cc c@z.io] --subject "..." --body "..." | --body-file <path> [--in-reply-to <message-id>]');
|
|
7026
|
+
if (toRaw.length === 0 || !subject || !bodyFlag && !bodyFile) {
|
|
7027
|
+
console.error('用法:crab mail send --to a@x.cc|联系人名[,b@y.cn] [--cc c@z.io] --subject "..." --body "..." | --body-file <path> [--attach <云盘id|文件名>,...] [--in-reply-to <message-id>]');
|
|
7028
|
+
return EXIT_USAGE;
|
|
7029
|
+
}
|
|
7030
|
+
if (attachRaw.length > 10) {
|
|
7031
|
+
console.error("附件最多 10 个。");
|
|
6577
7032
|
return EXIT_USAGE;
|
|
6578
7033
|
}
|
|
6579
7034
|
let body = bodyFlag ?? "";
|
|
@@ -6588,10 +7043,29 @@ async function mailSend() {
|
|
|
6588
7043
|
const credentials = requireCredentials();
|
|
6589
7044
|
if (typeof credentials === "number")
|
|
6590
7045
|
return credentials;
|
|
7046
|
+
const to = await resolveRecipients(credentials, toRaw);
|
|
7047
|
+
if (to === null)
|
|
7048
|
+
return 1;
|
|
7049
|
+
const cc = ccRaw.length > 0 ? await resolveRecipients(credentials, ccRaw) : [];
|
|
7050
|
+
if (cc === null)
|
|
7051
|
+
return 1;
|
|
7052
|
+
const attachmentIds = [];
|
|
7053
|
+
for (const token of attachRaw) {
|
|
7054
|
+
const hit = await resolveStorageObject(credentials, token);
|
|
7055
|
+
if (typeof hit === "number")
|
|
7056
|
+
return hit;
|
|
7057
|
+
if (typeof hit === "object" && hit.isEncrypted) {
|
|
7058
|
+
console.error(`「${token}」是端到端加密文件,暂不能作为邮件附件发送(服务端无法解密)。`);
|
|
7059
|
+
return EXIT_USAGE;
|
|
7060
|
+
}
|
|
7061
|
+
const id = typeof hit === "object" ? hit.id : hit;
|
|
7062
|
+
if (!attachmentIds.includes(id))
|
|
7063
|
+
attachmentIds.push(id);
|
|
7064
|
+
}
|
|
6591
7065
|
const res = await api2(credentials.apiBase, "/v1/mail/messages", {
|
|
6592
7066
|
method: "POST",
|
|
6593
7067
|
headers: authHeaders(credentials),
|
|
6594
|
-
body: JSON.stringify({ to, cc, subject, body, ...inReplyTo ? { inReplyTo } : {} })
|
|
7068
|
+
body: JSON.stringify({ to, cc, subject, body, ...inReplyTo ? { inReplyTo } : {}, ...attachmentIds.length > 0 ? { attachmentIds } : {} })
|
|
6595
7069
|
});
|
|
6596
7070
|
if (res.status !== 200 && res.status !== 201) {
|
|
6597
7071
|
console.error(errorMessage(res.body, "发送失败。"));
|
|
@@ -6603,8 +7077,10 @@ async function mailSend() {
|
|
|
6603
7077
|
return 0;
|
|
6604
7078
|
}
|
|
6605
7079
|
console.log(`✓ 已发送(id ${result.message.id})`);
|
|
7080
|
+
if (attachmentIds.length > 0)
|
|
7081
|
+
console.log(` 附件 ${attachmentIds.length} 个(云盘引用,不复制)`);
|
|
6606
7082
|
if (result.chargedCredits > 0)
|
|
6607
|
-
console.log(` 消耗积分:${result.chargedCredits}`);
|
|
7083
|
+
console.log(` 消耗积分:${fmtCredits(result.chargedCredits)}`);
|
|
6608
7084
|
if (result.failedExternal > 0)
|
|
6609
7085
|
console.log(` ⚠ ${result.failedExternal} 个外部收件人发送失败,对应积分已退回`);
|
|
6610
7086
|
return 0;
|
|
@@ -6824,11 +7300,12 @@ function formatBytesCli(n) {
|
|
|
6824
7300
|
function printStorageRow(o) {
|
|
6825
7301
|
const flags = [
|
|
6826
7302
|
o.source !== "files" ? o.source === "media" ? "素材" : "邮件附件" : null,
|
|
7303
|
+
o.isEncrypted ? "加密" : null,
|
|
6827
7304
|
o.isPublic ? "公开" : null
|
|
6828
7305
|
].filter(Boolean);
|
|
6829
7306
|
console.log(`${o.id} ${o.filename} ${formatBytesCli(o.sizeBytes)} ${formatTime(o.createdAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
|
|
6830
7307
|
}
|
|
6831
|
-
async function
|
|
7308
|
+
async function resolveStorageObject(credentials, token) {
|
|
6832
7309
|
for (const trash of [false, true]) {
|
|
6833
7310
|
const params = new URLSearchParams({ limit: "100", q: token });
|
|
6834
7311
|
if (trash)
|
|
@@ -6846,10 +7323,16 @@ async function resolveStorageId(credentials, token) {
|
|
|
6846
7323
|
return EXIT_USAGE;
|
|
6847
7324
|
}
|
|
6848
7325
|
if (exact.length === 1)
|
|
6849
|
-
return exact[0]
|
|
7326
|
+
return exact[0];
|
|
6850
7327
|
}
|
|
6851
7328
|
return token;
|
|
6852
7329
|
}
|
|
7330
|
+
async function resolveStorageId(credentials, token) {
|
|
7331
|
+
const hit = await resolveStorageObject(credentials, token);
|
|
7332
|
+
if (typeof hit === "string" || typeof hit === "number")
|
|
7333
|
+
return hit;
|
|
7334
|
+
return hit.id;
|
|
7335
|
+
}
|
|
6853
7336
|
async function storageList() {
|
|
6854
7337
|
const credentials = requireCredentials();
|
|
6855
7338
|
if (typeof credentials === "number")
|
|
@@ -6891,6 +7374,9 @@ async function storageList() {
|
|
|
6891
7374
|
const q = readFlag("--q");
|
|
6892
7375
|
if (q)
|
|
6893
7376
|
params.set("q", q);
|
|
7377
|
+
const messageId = readFlag("--message-id");
|
|
7378
|
+
if (messageId)
|
|
7379
|
+
params.set("messageId", messageId);
|
|
6894
7380
|
const res = await api2(credentials.apiBase, `/v1/storage?${params}`, { headers: authHeaders(credentials) });
|
|
6895
7381
|
if (res.status !== 200) {
|
|
6896
7382
|
console.error(errorMessage(res.body, "获取云盘列表失败。"));
|
|
@@ -6907,10 +7393,65 @@ async function storageList() {
|
|
|
6907
7393
|
}
|
|
6908
7394
|
for (const object of page.objects)
|
|
6909
7395
|
printStorageRow(object);
|
|
7396
|
+
console.log(`共 ${page.total} 条`);
|
|
6910
7397
|
if (page.nextOffset !== null)
|
|
6911
7398
|
console.log(`(还有更多:storage ls --offset ${page.nextOffset})`);
|
|
6912
7399
|
return 0;
|
|
6913
7400
|
}
|
|
7401
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
7402
|
+
var MASTER_AAD = utf82("drive-master");
|
|
7403
|
+
var DATA_AAD = utf82("drive-data");
|
|
7404
|
+
var KEK_ITERATIONS = 210000;
|
|
7405
|
+
function drivePassphrase() {
|
|
7406
|
+
return readFlag("--passphrase") ?? process.env.CRAB_PASSPHRASE;
|
|
7407
|
+
}
|
|
7408
|
+
async function driveMasterKey(credentials) {
|
|
7409
|
+
const passphrase = drivePassphrase();
|
|
7410
|
+
if (!passphrase) {
|
|
7411
|
+
console.error("需要云盘口令:--passphrase <口令> 或环境变量 CRAB_PASSPHRASE。");
|
|
7412
|
+
return EXIT_USAGE;
|
|
7413
|
+
}
|
|
7414
|
+
const res = await api2(credentials.apiBase, "/v1/storage/keys", { headers: authHeaders(credentials) });
|
|
7415
|
+
if (res.status === 404) {
|
|
7416
|
+
console.error("云盘口令尚未设置——正在初始化(恢复码只显示一次,请立即抄写):");
|
|
7417
|
+
const salt = b64urlEncode(randomKeyBytes(16));
|
|
7418
|
+
const masterRaw = randomKeyBytes(32);
|
|
7419
|
+
const recoveryCode = randomCode(16);
|
|
7420
|
+
const kek = await importAesKey(await deriveKekBits(passphrase, b64urlDecode(salt), KEK_ITERATIONS));
|
|
7421
|
+
const recoveryKek = await importAesKey(await deriveKekBits(recoveryCode, b64urlDecode(salt), KEK_ITERATIONS));
|
|
7422
|
+
const wrapped = await aeadEncrypt(kek, masterRaw, MASTER_AAD);
|
|
7423
|
+
const recoveryWrapped = await aeadEncrypt(recoveryKek, masterRaw, MASTER_AAD);
|
|
7424
|
+
const put = await api2(credentials.apiBase, "/v1/storage/keys", {
|
|
7425
|
+
method: "PUT",
|
|
7426
|
+
headers: authHeaders(credentials),
|
|
7427
|
+
body: JSON.stringify({
|
|
7428
|
+
kdfSalt: salt,
|
|
7429
|
+
kdfIterations: KEK_ITERATIONS,
|
|
7430
|
+
wrappedMasterKey: `u1.${sealToB64url({ iv: wrapped.iv, ciphertext: wrapped.ciphertext })}`,
|
|
7431
|
+
recoveryWrappedMasterKey: `r1.${sealToB64url({ iv: recoveryWrapped.iv, ciphertext: recoveryWrapped.ciphertext })}`
|
|
7432
|
+
})
|
|
7433
|
+
});
|
|
7434
|
+
if (put.status !== 200 && put.status !== 201) {
|
|
7435
|
+
console.error(errorMessage(put.body, "初始化云盘口令失败。"));
|
|
7436
|
+
return exitCodeForApiError(put.body);
|
|
7437
|
+
}
|
|
7438
|
+
console.log(`★ 恢复码:${recoveryCode}`);
|
|
7439
|
+
console.log(" (口令遗忘时的唯一找回方式;平台无后门,丢失即数据不可解)");
|
|
7440
|
+
return importAesKey(masterRaw);
|
|
7441
|
+
}
|
|
7442
|
+
if (res.status !== 200) {
|
|
7443
|
+
console.error(errorMessage(res.body, "获取云盘密钥失败。"));
|
|
7444
|
+
return exitCodeForApiError(res.body);
|
|
7445
|
+
}
|
|
7446
|
+
const keys = res.body.data;
|
|
7447
|
+
try {
|
|
7448
|
+
const kek = await importAesKey(await deriveKekBits(passphrase, b64urlDecode(keys.kdfSalt), keys.kdfIterations));
|
|
7449
|
+
return await importAesKey(await unwrapKeyBytes(kek, sealFromB64url(keys.wrappedMasterKey.slice(3)), MASTER_AAD));
|
|
7450
|
+
} catch {
|
|
7451
|
+
console.error("口令错误。");
|
|
7452
|
+
return 1;
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
6914
7455
|
async function storageUploadOne(credentials, filePath) {
|
|
6915
7456
|
let buf;
|
|
6916
7457
|
try {
|
|
@@ -6921,14 +7462,22 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
6921
7462
|
}
|
|
6922
7463
|
const filename = basename(filePath);
|
|
6923
7464
|
const mimeType = mimeFor(filename);
|
|
7465
|
+
const master = await driveMasterKey(credentials);
|
|
7466
|
+
if (typeof master === "number")
|
|
7467
|
+
return master;
|
|
7468
|
+
const dataKey = randomKeyBytes(32);
|
|
7469
|
+
const sealed = await aeadEncrypt(await importAesKey(dataKey), new Uint8Array(buf), DATA_AAD);
|
|
7470
|
+
const cipher = new Uint8Array([...sealed.iv, ...sealed.ciphertext]);
|
|
7471
|
+
const wrapped = await aeadEncrypt(master, dataKey, DATA_AAD);
|
|
7472
|
+
const wrappedDataKey = `u1.${sealToB64url({ iv: wrapped.iv, ciphertext: wrapped.ciphertext })}`;
|
|
6924
7473
|
const presign = await api2(credentials.apiBase, "/v1/storage/presign", {
|
|
6925
7474
|
method: "POST",
|
|
6926
7475
|
headers: authHeaders(credentials),
|
|
6927
|
-
body: JSON.stringify({ filename, mimeType, sizeBytes:
|
|
7476
|
+
body: JSON.stringify({ filename, mimeType, sizeBytes: cipher.byteLength, enc: true })
|
|
6928
7477
|
});
|
|
6929
7478
|
if (presign.status === 200) {
|
|
6930
7479
|
const { fileId, uploadUrl } = presign.body.data;
|
|
6931
|
-
const put = await fetch(uploadUrl, { method: "PUT", body:
|
|
7480
|
+
const put = await fetch(uploadUrl, { method: "PUT", body: cipher, headers: { "content-type": mimeType } });
|
|
6932
7481
|
if (!put.ok) {
|
|
6933
7482
|
console.error(`直传失败(${put.status}):${filename}`);
|
|
6934
7483
|
return 1;
|
|
@@ -6936,7 +7485,7 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
6936
7485
|
const complete = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(fileId)}/complete`, {
|
|
6937
7486
|
method: "POST",
|
|
6938
7487
|
headers: authHeaders(credentials),
|
|
6939
|
-
body:
|
|
7488
|
+
body: JSON.stringify({ isEncrypted: true, wrappedDataKey })
|
|
6940
7489
|
});
|
|
6941
7490
|
if (complete.status !== 200 && complete.status !== 201) {
|
|
6942
7491
|
console.error(errorMessage(complete.body, `记账失败:${filename}`));
|
|
@@ -6946,12 +7495,14 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
6946
7495
|
if (hasFlag("--json"))
|
|
6947
7496
|
console.log(JSON.stringify(object, null, 2));
|
|
6948
7497
|
else
|
|
6949
|
-
console.log(`✓
|
|
7498
|
+
console.log(`✓ 已上传(已加密):${object.filename}(id ${object.id})`);
|
|
6950
7499
|
return 0;
|
|
6951
7500
|
}
|
|
6952
7501
|
if (presign.body?.error?.code === "presign_unavailable") {
|
|
6953
7502
|
const form = new FormData;
|
|
6954
|
-
form.append("file", new Blob([
|
|
7503
|
+
form.append("file", new Blob([cipher], { type: mimeType }), filename);
|
|
7504
|
+
form.append("enc", "1");
|
|
7505
|
+
form.append("wrappedDataKey", wrappedDataKey);
|
|
6955
7506
|
const res = await fetch(`${credentials.apiBase}/v1/storage`, {
|
|
6956
7507
|
method: "POST",
|
|
6957
7508
|
headers: authHeaders(credentials),
|
|
@@ -6965,16 +7516,24 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
6965
7516
|
if (hasFlag("--json"))
|
|
6966
7517
|
console.log(JSON.stringify(body.data, null, 2));
|
|
6967
7518
|
else
|
|
6968
|
-
console.log(`✓
|
|
7519
|
+
console.log(`✓ 已上传(已加密):${body.data?.filename}(id ${body.data?.id})`);
|
|
6969
7520
|
return 0;
|
|
6970
7521
|
}
|
|
6971
7522
|
console.error(errorMessage(presign.body, `上传失败:${filename}`));
|
|
6972
7523
|
return exitCodeForApiError(presign.body);
|
|
6973
7524
|
}
|
|
6974
7525
|
async function storageUpload(paths) {
|
|
6975
|
-
const files =
|
|
7526
|
+
const files = [];
|
|
7527
|
+
for (let i = 0;i < paths.length; i++) {
|
|
7528
|
+
if (paths[i].startsWith("--")) {
|
|
7529
|
+
if (paths[i] === "--passphrase")
|
|
7530
|
+
i++;
|
|
7531
|
+
continue;
|
|
7532
|
+
}
|
|
7533
|
+
files.push(paths[i]);
|
|
7534
|
+
}
|
|
6976
7535
|
if (files.length === 0) {
|
|
6977
|
-
console.error("用法:crab storage upload <file...> [--json]");
|
|
7536
|
+
console.error("用法:crab storage upload <file...> [--passphrase 口令] [--json]");
|
|
6978
7537
|
return EXIT_USAGE;
|
|
6979
7538
|
}
|
|
6980
7539
|
const credentials = requireCredentials();
|
|
@@ -6992,9 +7551,33 @@ async function storageUpload(paths) {
|
|
|
6992
7551
|
}
|
|
6993
7552
|
async function storageDownload(token) {
|
|
6994
7553
|
if (!token) {
|
|
6995
|
-
console.error("用法:crab storage download <id
|
|
7554
|
+
console.error("用法:crab storage download <id|文件名|分享链接> [-o 输出路径] [--passphrase 口令] [--passcode 提取码]");
|
|
6996
7555
|
return EXIT_USAGE;
|
|
6997
7556
|
}
|
|
7557
|
+
const urlMatch = token.match(/\/s\/([0-9a-f]{64})(?:#([A-Za-z0-9_-]+))?$/);
|
|
7558
|
+
const shareToken = urlMatch?.[1];
|
|
7559
|
+
const fragment = urlMatch?.[2];
|
|
7560
|
+
if (shareToken) {
|
|
7561
|
+
const apiBase = process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
|
|
7562
|
+
const passcode = readFlag("--passcode");
|
|
7563
|
+
const url = `${apiBase}/s/${shareToken}${passcode ? `?passcode=${encodeURIComponent(passcode)}` : ""}`;
|
|
7564
|
+
const res = await fetch(url);
|
|
7565
|
+
if (!res.ok) {
|
|
7566
|
+
const body = await res.json().catch(() => ({}));
|
|
7567
|
+
console.error(errorMessage(body, "下载失败。"));
|
|
7568
|
+
return exitCodeForApiError(body);
|
|
7569
|
+
}
|
|
7570
|
+
let bytes = new Uint8Array(await res.arrayBuffer());
|
|
7571
|
+
if (fragment) {
|
|
7572
|
+
bytes = await aeadDecrypt(await importAesKey(b64urlDecode(fragment)), { iv: bytes.slice(0, 12), ciphertext: bytes.slice(12) }, DATA_AAD);
|
|
7573
|
+
}
|
|
7574
|
+
const disposition = res.headers.get("content-disposition") ?? "";
|
|
7575
|
+
const m = disposition.match(/filename\*=UTF-8''([^;]+)/);
|
|
7576
|
+
const out = readFlag("-o") ?? readFlag("--out") ?? (m ? decodeURIComponent(m[1]) : `${shareToken}.bin`);
|
|
7577
|
+
writeFileSync2(out, Buffer.from(bytes));
|
|
7578
|
+
console.log(`✓ 已下载${fragment ? "并解密" : ""}:${out}(${formatBytesCli(bytes.byteLength)})`);
|
|
7579
|
+
return 0;
|
|
7580
|
+
}
|
|
6998
7581
|
const credentials = requireCredentials();
|
|
6999
7582
|
if (typeof credentials === "number")
|
|
7000
7583
|
return credentials;
|
|
@@ -7009,12 +7592,26 @@ async function storageDownload(token) {
|
|
|
7009
7592
|
console.error(errorMessage(body, "下载失败。"));
|
|
7010
7593
|
return exitCodeForApiError(body);
|
|
7011
7594
|
}
|
|
7595
|
+
let bytes = new Uint8Array(await res.arrayBuffer());
|
|
7596
|
+
const wrapped = res.headers.get("x-drive-wrapped-key");
|
|
7597
|
+
if (wrapped?.startsWith("u1.")) {
|
|
7598
|
+
const passphrase = drivePassphrase();
|
|
7599
|
+
if (!passphrase) {
|
|
7600
|
+
console.error("该文件已加密:--passphrase <口令> 或 CRAB_PASSPHRASE 提供云盘口令后重试。");
|
|
7601
|
+
return EXIT_USAGE;
|
|
7602
|
+
}
|
|
7603
|
+
const master = await driveMasterKey(credentials);
|
|
7604
|
+
if (typeof master === "number")
|
|
7605
|
+
return master;
|
|
7606
|
+
const dataKey = await unwrapKeyBytes(master, sealFromB64url(wrapped.slice(3)), DATA_AAD);
|
|
7607
|
+
bytes = await aeadDecrypt(await importAesKey(dataKey), { iv: bytes.slice(0, 12), ciphertext: bytes.slice(12) }, DATA_AAD);
|
|
7608
|
+
}
|
|
7012
7609
|
const disposition = res.headers.get("content-disposition") ?? "";
|
|
7013
7610
|
const match = disposition.match(/filename\*=UTF-8''([^;]+)/);
|
|
7014
7611
|
const fallbackName = match ? decodeURIComponent(match[1]) : `${id}.bin`;
|
|
7015
7612
|
const out = readFlag("-o") ?? readFlag("--out") ?? fallbackName;
|
|
7016
|
-
writeFileSync2(out, Buffer.from(
|
|
7017
|
-
console.log(`✓ 已下载:${out}(${formatBytesCli(
|
|
7613
|
+
writeFileSync2(out, Buffer.from(bytes));
|
|
7614
|
+
console.log(`✓ 已下载:${out}(${formatBytesCli(bytes.byteLength)})`);
|
|
7018
7615
|
return 0;
|
|
7019
7616
|
}
|
|
7020
7617
|
async function storagePatch(credentials, token, patch) {
|
|
@@ -7116,6 +7713,110 @@ async function storageMove(action, token) {
|
|
|
7116
7713
|
console.log(`✓ ${action === "rm" ? "已移入回收站(7 天后自动清理)" : action === "restore" ? "已恢复" : "已彻底删除"}(${id})`);
|
|
7117
7714
|
return 0;
|
|
7118
7715
|
}
|
|
7716
|
+
async function storageShareCmd(token) {
|
|
7717
|
+
if (!token) {
|
|
7718
|
+
console.error("用法:crab storage share <id|文件名> [--expires 7|30|90|forever] [--passcode 提取码]");
|
|
7719
|
+
return EXIT_USAGE;
|
|
7720
|
+
}
|
|
7721
|
+
const expires = readFlag("--expires") ?? "7";
|
|
7722
|
+
const passcode = readFlag("--passcode");
|
|
7723
|
+
if (!["7", "30", "90", "forever"].includes(expires) || passcode !== undefined && !/^[A-Za-z0-9]{4,8}$/.test(passcode)) {
|
|
7724
|
+
console.error("无效的 --expires / --passcode 参数。");
|
|
7725
|
+
return EXIT_USAGE;
|
|
7726
|
+
}
|
|
7727
|
+
const credentials = requireCredentials();
|
|
7728
|
+
if (typeof credentials === "number")
|
|
7729
|
+
return credentials;
|
|
7730
|
+
const hit = await resolveStorageObject(credentials, token);
|
|
7731
|
+
if (typeof hit === "number")
|
|
7732
|
+
return hit;
|
|
7733
|
+
const id = typeof hit === "object" ? hit.id : hit;
|
|
7734
|
+
const needMaster = typeof hit === "object" && hit.isEncrypted === true && (hit.wrappedDataKey ?? "").startsWith("u1.");
|
|
7735
|
+
const master = needMaster ? await driveMasterKey(credentials) : null;
|
|
7736
|
+
if (needMaster && typeof master === "number")
|
|
7737
|
+
return master;
|
|
7738
|
+
const res = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(id)}/share`, {
|
|
7739
|
+
method: "POST",
|
|
7740
|
+
headers: authHeaders(credentials),
|
|
7741
|
+
body: JSON.stringify({ expires, ...passcode ? { passcode } : {} })
|
|
7742
|
+
});
|
|
7743
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
7744
|
+
console.error(errorMessage(res.body, "创建分享失败。"));
|
|
7745
|
+
return exitCodeForApiError(res.body);
|
|
7746
|
+
}
|
|
7747
|
+
const data = res.body.data;
|
|
7748
|
+
let link = data.shareUrl;
|
|
7749
|
+
if (data.isEncrypted && data.wrappedDataKey) {
|
|
7750
|
+
const masterKey = master ?? await driveMasterKey(credentials);
|
|
7751
|
+
if (typeof masterKey === "number") {
|
|
7752
|
+
await api2(credentials.apiBase, `/v1/storage/shares/${encodeURIComponent(data.share.id)}`, {
|
|
7753
|
+
method: "DELETE",
|
|
7754
|
+
headers: authHeaders(credentials)
|
|
7755
|
+
}).catch(() => null);
|
|
7756
|
+
console.error("该文件是端到端加密文件,需要云盘口令解出分享钥匙:--passphrase <口令> 或 CRAB_PASSPHRASE(本次分享已撤销,未留孤儿链接)。");
|
|
7757
|
+
return EXIT_USAGE;
|
|
7758
|
+
}
|
|
7759
|
+
const dataKey = await unwrapKeyBytes(masterKey, sealFromB64url(data.wrappedDataKey.slice(3)), DATA_AAD);
|
|
7760
|
+
link = `${link}#${b64urlEncode(dataKey)}`;
|
|
7761
|
+
}
|
|
7762
|
+
console.log(`✓ 分享链接已创建(${expires === "forever" ? "永久" : `${expires} 天`}有效):`);
|
|
7763
|
+
console.log(` ${link}`);
|
|
7764
|
+
if (data.share.hasPasscode)
|
|
7765
|
+
console.log(" (已设提取码,接收方需携带)");
|
|
7766
|
+
if (data.isEncrypted)
|
|
7767
|
+
console.log(" (链接自带解密钥匙,泄露即泄露文件——请走可信渠道传递)");
|
|
7768
|
+
return 0;
|
|
7769
|
+
}
|
|
7770
|
+
async function storageShares(token) {
|
|
7771
|
+
if (!token) {
|
|
7772
|
+
console.error("用法:crab storage shares <id|文件名>");
|
|
7773
|
+
return EXIT_USAGE;
|
|
7774
|
+
}
|
|
7775
|
+
const credentials = requireCredentials();
|
|
7776
|
+
if (typeof credentials === "number")
|
|
7777
|
+
return credentials;
|
|
7778
|
+
const id = await resolveStorageId(credentials, token);
|
|
7779
|
+
if (typeof id === "number")
|
|
7780
|
+
return id;
|
|
7781
|
+
const res = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(id)}/shares`, { headers: authHeaders(credentials) });
|
|
7782
|
+
if (res.status !== 200) {
|
|
7783
|
+
console.error(errorMessage(res.body, "获取分享列表失败。"));
|
|
7784
|
+
return exitCodeForApiError(res.body);
|
|
7785
|
+
}
|
|
7786
|
+
const rows = res.body.data.shares;
|
|
7787
|
+
if (hasFlag("--json")) {
|
|
7788
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7789
|
+
return 0;
|
|
7790
|
+
}
|
|
7791
|
+
if (rows.length === 0) {
|
|
7792
|
+
console.log("该文件还没有分享链接。");
|
|
7793
|
+
return 0;
|
|
7794
|
+
}
|
|
7795
|
+
for (const s of rows) {
|
|
7796
|
+
const state = s.revokedAt ? "已撤销" : s.expiresAt !== null && s.expiresAt <= Date.now() ? "已过期" : "生效中";
|
|
7797
|
+
console.log(`${s.id} …${s.token.slice(-12)} ${state} 下载 ${s.downloadCount} 次 ${formatTime(s.createdAt)}${s.hasPasscode ? " [提取码]" : ""}`);
|
|
7798
|
+
}
|
|
7799
|
+
return 0;
|
|
7800
|
+
}
|
|
7801
|
+
async function storageUnshare(shareId) {
|
|
7802
|
+
if (!shareId) {
|
|
7803
|
+
console.error("用法:crab storage unshare <shareId>(id 见 crab storage shares)");
|
|
7804
|
+
return EXIT_USAGE;
|
|
7805
|
+
}
|
|
7806
|
+
const credentials = requireCredentials();
|
|
7807
|
+
if (typeof credentials === "number")
|
|
7808
|
+
return credentials;
|
|
7809
|
+
const res = await api2(credentials.apiBase, `/v1/storage/shares/${encodeURIComponent(shareId)}`, {
|
|
7810
|
+
method: "DELETE",
|
|
7811
|
+
headers: authHeaders(credentials)
|
|
7812
|
+
});
|
|
7813
|
+
if (res.status !== 200) {
|
|
7814
|
+
console.error(errorMessage(res.body, "撤销失败。"));
|
|
7815
|
+
return exitCodeForApiError(res.body);
|
|
7816
|
+
}
|
|
7817
|
+
console.log(`✓ 已撤销分享(${shareId})。`);
|
|
7818
|
+
return 0;
|
|
7819
|
+
}
|
|
7119
7820
|
async function storageQuota() {
|
|
7120
7821
|
const credentials = requireCredentials();
|
|
7121
7822
|
if (typeof credentials === "number")
|
|
@@ -7323,8 +8024,27 @@ switch (command) {
|
|
|
7323
8024
|
process.exit(await storageMkCat(args.slice(2).join(" ") || undefined));
|
|
7324
8025
|
if (sub === "tags")
|
|
7325
8026
|
process.exit(await storageTags());
|
|
8027
|
+
if (sub === "share")
|
|
8028
|
+
process.exit(await storageShareCmd(args[2]));
|
|
8029
|
+
if (sub === "shares")
|
|
8030
|
+
process.exit(await storageShares(args[2]));
|
|
8031
|
+
if (sub === "unshare")
|
|
8032
|
+
process.exit(await storageUnshare(args[2]));
|
|
7326
8033
|
console.error(sub ? `未知子命令:storage ${sub}
|
|
7327
|
-
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | tags
|
|
8034
|
+
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | tags | share | shares | unshare
|
|
8035
|
+
`);
|
|
8036
|
+
console.log(usage);
|
|
8037
|
+
process.exit(EXIT_USAGE);
|
|
8038
|
+
}
|
|
8039
|
+
case "credits": {
|
|
8040
|
+
const sub = args[1];
|
|
8041
|
+
if (!sub || sub === "balance")
|
|
8042
|
+
process.exit(await creditsBalance());
|
|
8043
|
+
if (sub === "history")
|
|
8044
|
+
process.exit(await creditsHistory());
|
|
8045
|
+
if (sub === "pricing")
|
|
8046
|
+
process.exit(await creditsPricing());
|
|
8047
|
+
console.error(`未知子命令:credits ${sub}
|
|
7328
8048
|
`);
|
|
7329
8049
|
console.log(usage);
|
|
7330
8050
|
process.exit(EXIT_USAGE);
|
|
@@ -30,6 +30,9 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
|
|
|
30
30
|
|---|---|
|
|
31
31
|
| 看账号身份 / 订阅 / 积分 | `crab whoami` |
|
|
32
32
|
| 程序化读取身份 | `crab whoami --json` |
|
|
33
|
+
| 积分余额(月送/充值/合计) | `crab credits`(需 `credits.read`,脚本消费加 `--json`) |
|
|
34
|
+
| 积分流水(花了多少、花在哪) | `crab credits history [--limit N] [--offset N]`(变动 ±:消耗为负;`--json` 含 total) |
|
|
35
|
+
| 查动作计价(如发一封外域邮件扣多少) | `crab credits pricing`(公开端点,无需登录) |
|
|
33
36
|
| 列出已授权的 Agent 令牌 | `crab tokens list`(脚本消费加 `--json`) |
|
|
34
37
|
| 撤销某个令牌(高危,先确认) | `crab tokens revoke <id>` |
|
|
35
38
|
| 重新授权 / 调整 scope | `crab login --scopes mail`(组名整组授权;也可单 scope 如 `mail.read`。浏览器确认,你只发起) |
|
|
@@ -39,6 +42,10 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
|
|
|
39
42
|
| 我的邀请码、邀请链接与受邀名单 | `crab contacts invite [--reset]` / `crab contacts invitees` |
|
|
40
43
|
| 管理登录设备 / 会话、修改密码 | 网页个人中心(crabcloud.cc/account)——CLI 有意不开放 |
|
|
41
44
|
|
|
45
|
+
积分命令需 `credits.read` scope(`account` 组含它):退出码 4 报缺该 scope 时,
|
|
46
|
+
引导 `crab login --scopes account` 重新授权。积分额度一律以**积分数**表述
|
|
47
|
+
(100 积分 = $1.00,仅价格类口径用美元);充值/下单是 Web 会话能力,CLI/Agent
|
|
48
|
+
令牌不动账——用户要充值时引导网页账单页(crabcloud.cc/account/billing)。
|
|
42
49
|
拿不准令牌 id 时先 `crab tokens list` 核对;`revoke` 只接受列表里的 id。
|
|
43
50
|
|
|
44
51
|
## 机器可读输出
|
|
@@ -12,7 +12,12 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
|
|
|
12
12
|
|
|
13
13
|
## 第一步:确认可用
|
|
14
14
|
|
|
15
|
-
- `crab --version`
|
|
15
|
+
- 运行 `crab --version` 确认已安装。命令不存在时让用户运行
|
|
16
|
+
`npx @crabcloud/cli init`(登录 + 安装本 skill 一步完成);已装过但命令缺失
|
|
17
|
+
时运行 `npx @crabcloud/cli@latest init` 升级。
|
|
18
|
+
- CLI 基于 npx 分发,**本机没有 `crab` 命令时,下文所有 `crab <命令>` 都可以
|
|
19
|
+
`npx @crabcloud/cli <命令>` 等价执行**(例如 `npx @crabcloud/cli mail list`)。
|
|
20
|
+
- 未登录(退出码 3)引导用户 `crab login`。
|
|
16
21
|
- 邮箱动作需要对应 scope:读 `mail.read`、搜索 `mail.search`、发信 `mail.send`、
|
|
17
22
|
删除 `mail.delete`。退出码 4 = scope 不足:如实转述缺哪个,引导用户
|
|
18
23
|
`crab login --scopes mail`(组名 = 四项整组)或单列所需 scope 按需最小化
|
|
@@ -28,6 +33,8 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
|
|
|
28
33
|
| 读某封邮件 | `crab mail read <id>`(自动置已读;正文 + 操作记录) |
|
|
29
34
|
| 找邮件 | `crab mail search <关键词>`(主题/发件人/收件人/正文,不含废纸篓) |
|
|
30
35
|
| 发邮件 | `crab mail send --to a@x.cc --subject "..." --body "..."`(长正文用 `--body-file`) |
|
|
36
|
+
| 发邮件带附件 | `crab mail send … --attach <云盘id\|文件名>[,…]`(引用云盘已有对象,不复制,≤10 个;先 `crab storage ls` 核对对象,文件名需精确唯一命中) |
|
|
37
|
+
| 按名字/备注发邮件 | `--to`/`--cc` 可直接写联系人名(不含 `@`):CLI 查联系人簿解析成地址,唯一命中即用;多候选会报错列出,改用完整地址重试。也可先 `crab contacts list <名字>`(`crabcloud` skill)查地址再发 |
|
|
31
38
|
| 回信(带线程锚点) | 先 `crab mail read <id>` 取 Message-ID,再 `crab mail send --to … --in-reply-to <message-id> …` |
|
|
32
39
|
| 整理邮箱 | `crab mail archive <id>` / `crab mail trash <id>` / `crab mail delete <id>` |
|
|
33
40
|
|
|
@@ -47,15 +54,21 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
|
|
|
47
54
|
|
|
48
55
|
- `mail list --json` → `{ items: [...], nextCursor }`,条目含
|
|
49
56
|
`id/folder/fromAddr/to/subject/snippet/isUnread/hasAttachments/createdAt`;
|
|
50
|
-
- `mail read --json` → 完整正文 + `
|
|
57
|
+
- `mail read --json` → 完整正文 + `attachments`(id/filename/mimeType/sizeBytes)
|
|
58
|
+
+ `actors`(谁读过/谁代发,审计源);
|
|
51
59
|
- `mail send --json` → `{ message, chargedCredits, failedExternal }`。
|
|
52
60
|
|
|
53
61
|
## 纪律
|
|
54
62
|
|
|
55
63
|
- **发邮件是外发动作**:发送前向用户复述收件人、主题与计费口径,确认后再执行;
|
|
56
|
-
|
|
64
|
+
不确定收件人身份时优先查联系人簿(`crab contacts list <名字>`,见 `crabcloud`
|
|
65
|
+
skill),其次搜索历史邮件核对,不要猜测地址。
|
|
57
66
|
- **`mail delete` 是彻底删除**,不可恢复;用户没明说「彻底删除」时用
|
|
58
67
|
`mail trash`(可找回,废纸篓 30 天后自动清除)。
|
|
59
68
|
- 草稿流程:先 `mail list --folder drafts` 查看草稿;本阶段 CLI 不直接改草稿,
|
|
60
69
|
确认发送请把草稿内容转述给用户后用 `mail send` 发出。
|
|
61
|
-
-
|
|
70
|
+
- 附件:`mail read` 输出附件清单(id/文件名/类型/体积);附件内容归云盘管
|
|
71
|
+
——下载用 `crab storage download <id>`(见 `crabcloud-storage` skill,跨来
|
|
72
|
+
源通用),发送引用云盘对象用 `mail send --attach`(对象须未被其他邮件绑
|
|
73
|
+
定;若报错说明已被引用,改走正文链接)。邮件详情附件显示「已在云盘删除」
|
|
74
|
+
表示对象已被软删,如实转述。
|
|
@@ -12,7 +12,12 @@ description: Manage the user's Crab Cloud Drive (云盘 — unified storage for
|
|
|
12
12
|
|
|
13
13
|
## 第一步:确认可用
|
|
14
14
|
|
|
15
|
-
- `crab --version`
|
|
15
|
+
- 运行 `crab --version` 确认已安装。命令不存在时让用户运行
|
|
16
|
+
`npx @crabcloud/cli init`(登录 + 安装本 skill 一步完成);已装过但命令缺失
|
|
17
|
+
时运行 `npx @crabcloud/cli@latest init` 升级。
|
|
18
|
+
- CLI 基于 npx 分发,**本机没有 `crab` 命令时,下文所有 `crab <命令>` 都可以
|
|
19
|
+
`npx @crabcloud/cli <命令>` 等价执行**(例如 `npx @crabcloud/cli storage ls`)。
|
|
20
|
+
- 未登录(退出码 3)引导用户 `crab login`。
|
|
16
21
|
- 动作需要对应 scope:列取/下载 `storage.read`,上传/改名/标签/分类/公开
|
|
17
22
|
`storage.write`,移回收站/恢复/彻底删除 `storage.delete`。退出码 4 = scope
|
|
18
23
|
不足:如实转述缺哪个,引导 `crab login --scopes storage`(组名 = 三项整组)
|
|
@@ -33,6 +38,7 @@ description: Manage the user's Crab Cloud Drive (云盘 — unified storage for
|
|
|
33
38
|
| 加/删标签 | `crab storage tag <id\|文件名> --add a,b [--remove c,d]` |
|
|
34
39
|
| 归类 / 建分类 | `crab storage category <id\|文件名> <categoryId\|none>`;`crab storage mkcat <名称>`;分类 id 见 `crab storage categories` |
|
|
35
40
|
| 公开分享 | `crab storage publish <id\|文件名>`(打印公开直链;收回用 `unpublish`) |
|
|
41
|
+
| 可控分享(有效期/提取码/撤销) | `crab storage share <id\|文件名> [--expires 7\|30\|90\|forever] [--passcode 提取码]`(打印完整分享链接;加密对象链接自带 `#` 密钥片段);列表 `crab storage shares <id\|文件名>`、撤销 `crab storage unshare <shareId>` |
|
|
36
42
|
| 删除 | `crab storage rm <id\|文件名>`(进回收站,7 天后自动清理)→ `restore` 恢复 / `purge` 彻底删除 |
|
|
37
43
|
| 看水位 | `crab storage quota`(三来源分项 + 回收站占用 + 对象计数) |
|
|
38
44
|
|
|
@@ -44,7 +50,7 @@ restore/purge 直接用文件名即可);命中多个或查不到时按字面
|
|
|
44
50
|
|
|
45
51
|
程序化消费**始终加 `--json`**:
|
|
46
52
|
|
|
47
|
-
- `storage ls --json` → `{ objects: [...], nextOffset }`,条目含
|
|
53
|
+
- `storage ls --json` → `{ objects: [...], nextOffset, total }`,条目含
|
|
48
54
|
`id / source / filename / mimeType / sizeBytes / categoryId / tags / isPublic / createdAt`;
|
|
49
55
|
- `storage quota --json` → `{ usedBytes, quotaBytes, bySource: { files, media, mail }, trashBytes, counts }`;
|
|
50
56
|
- `upload` / `categories` / `tags` 的 `--json` 同构(单对象 / 分类数组 / 标签数组)。
|
|
@@ -53,6 +59,9 @@ restore/purge 直接用文件名即可);命中多个或查不到时按字面
|
|
|
53
59
|
|
|
54
60
|
- **publish 是对外公开**:公开后任何持链接者可访问,先向用户复述对象与后果,
|
|
55
61
|
确认后再执行;收回用 `unpublish`(已缓存浏览器至多 5 分钟内失效)。
|
|
62
|
+
- **share 是可控分享**:默认 7 天有效、可选提取码、随时 `unshare` 撤销;加密
|
|
63
|
+
对象的分享链接自带解密钥匙(`#` 后缀)——泄露链接即泄露文件,外发前向
|
|
64
|
+
用户复述有效期与提取码设置,确认后再执行。
|
|
56
65
|
- **purge 是彻底删除**,不可恢复;用户没明说「彻底删除」时一律用 `rm`
|
|
57
66
|
(回收站保留 7 天可找回)。
|
|
58
67
|
- 邮件附件与用户上传同账:水位告急时用 `quota` 分项定位来源再建议清理。
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crabcloud/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Crab Cloud CLI (crab)
|
|
5
|
+
"description": "Crab Cloud CLI (crab) \u2014 agent-first entry to your personal cloud, with skills bundled for Claude Code / Codex / ZCode.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"crab": "dist/crab.js"
|
|
8
8
|
},
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@crabcloud/cloudkit": "workspace:*",
|
|
27
28
|
"@crabcloud/contracts": "workspace:*",
|
|
28
29
|
"@crabcloud/skills": "workspace:*",
|
|
29
30
|
"@types/bun": "latest",
|