@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("CNY"),
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,b@y.cn [--cc c@z.io] --subject "..." --body "..." | --body-file <path>
6272
- [--in-reply-to <message-id>] 回信线程锚点(原信 RFC Message-ID,crab mail read 可见)
6273
- 发送邮件:平台内(@crabcloud.cc)免费,外部出站 2 积分/封
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 to = (readFlag("--to") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
6570
- const cc = (readFlag("--cc") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
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 (to.length === 0 || !subject || !bodyFlag && !bodyFile) {
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 resolveStorageId(credentials, token) {
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].id;
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: buf.byteLength })
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: new Uint8Array(buf), headers: { "content-type": mimeType } });
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(`✓ 已上传:${object.filename}(id ${object.id})`);
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([new Uint8Array(buf)], { type: mimeType }), filename);
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(`✓ 已上传:${body.data?.filename}(id ${body.data?.id})`);
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 = paths.filter((p) => !p.startsWith("--"));
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|文件名> [-o 输出路径]");
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(await res.arrayBuffer()));
7017
- console.log(`✓ 已下载:${out}(${formatBytesCli(Number(res.headers.get("content-length") ?? 0))})`);
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` 确认已安装;未登录(退出码 3)引导用户 `crab login`。
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` → 完整正文 + `actors`(谁读过/谁代发,审计源);
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
- - 附件:本阶段只标记 `hasAttachments`,附件内容尚未开放读取,如实说明。
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` 确认已安装;未登录(退出码 3)引导用户 `crab login`。
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.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
- "description": "Crab Cloud CLI (crab) — agent-first entry to your personal cloud, with skills bundled for Claude Code / Codex / ZCode.",
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",