@crabcloud/cli 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/crab.js CHANGED
@@ -2390,6 +2390,89 @@ var $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
2390
2390
  });
2391
2391
  };
2392
2392
  });
2393
+ function discriminatorMap(def) {
2394
+ const map = new Map;
2395
+ for (const option of def.options) {
2396
+ const values = option._zod.propValues?.[def.discriminator];
2397
+ if (!values || values.size === 0)
2398
+ throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
2399
+ for (const value of values) {
2400
+ if (map.has(value)) {
2401
+ if (value !== undefined)
2402
+ throw new Error(`Duplicate discriminator value "${String(value)}"`);
2403
+ map.set(value, null);
2404
+ } else {
2405
+ map.set(value, option);
2406
+ }
2407
+ }
2408
+ }
2409
+ return map;
2410
+ }
2411
+ var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnion", (inst, def) => {
2412
+ def.inclusive = false;
2413
+ $ZodUnion.init(inst, def);
2414
+ const _super = inst._zod.parse;
2415
+ defineLazyInternal(inst, "propValues", (zod) => {
2416
+ const propValues = {};
2417
+ let undefinedCount = 0;
2418
+ for (const option of zod.def.options) {
2419
+ const pv = option._zod.propValues;
2420
+ if (!pv || Object.keys(pv).length === 0)
2421
+ throw new Error(`Invalid discriminated union option at index "${zod.def.options.indexOf(option)}"`);
2422
+ if (pv[zod.def.discriminator]?.has(undefined))
2423
+ undefinedCount++;
2424
+ for (const [k, v] of Object.entries(pv)) {
2425
+ if (!Object.prototype.hasOwnProperty.call(propValues, k)) {
2426
+ assignProp(propValues, k, new Set);
2427
+ }
2428
+ for (const val of v) {
2429
+ propValues[k].add(val);
2430
+ }
2431
+ }
2432
+ }
2433
+ if (!zod.def.unionFallback && undefinedCount > 1)
2434
+ propValues[zod.def.discriminator]?.delete(undefined);
2435
+ return propValues;
2436
+ });
2437
+ def.options.forEach((option, i) => {
2438
+ const propShape = rawShape(option._zod.def);
2439
+ if (propShape && !Object.prototype.hasOwnProperty.call(propShape, def.discriminator)) {
2440
+ throw new Error(`Invalid discriminated union option at index "${i}"`);
2441
+ }
2442
+ });
2443
+ const disc = cached(() => discriminatorMap(def));
2444
+ inst._zod.parse = (payload, ctx) => {
2445
+ const input = payload.value;
2446
+ if (!isObject(input)) {
2447
+ payload.issues.push({
2448
+ code: "invalid_type",
2449
+ expected: "object",
2450
+ input,
2451
+ inst
2452
+ });
2453
+ return payload;
2454
+ }
2455
+ const value = input?.[def.discriminator];
2456
+ const opt = disc.value.get(value);
2457
+ if (opt && (value !== undefined || ctx.direction !== "backward")) {
2458
+ return opt._zod.run(payload, ctx);
2459
+ }
2460
+ if (def.unionFallback || ctx.direction === "backward") {
2461
+ return _super(payload, ctx);
2462
+ }
2463
+ payload.issues.push({
2464
+ code: "invalid_union",
2465
+ errors: [],
2466
+ note: "No matching discriminator",
2467
+ discriminator: def.discriminator,
2468
+ options: Array.from(disc.value.keys()).filter((value) => disc.value.get(value) !== null),
2469
+ input,
2470
+ path: [def.discriminator],
2471
+ inst
2472
+ });
2473
+ return payload;
2474
+ };
2475
+ });
2393
2476
  var $ZodIntersection = /* @__PURE__ */ $constructor("$ZodIntersection", (inst, def) => {
2394
2477
  $ZodType.init(inst, def);
2395
2478
  inst._zod.parse = (payload, ctx) => {
@@ -2682,6 +2765,28 @@ var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
2682
2765
  return payload;
2683
2766
  };
2684
2767
  });
2768
+ var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
2769
+ $ZodType.init(inst, def);
2770
+ const values = new Set(def.values);
2771
+ inst._zod.values = values;
2772
+ defineLazyInternal(inst, "pattern", (zod) => {
2773
+ const vals = zod.def.values;
2774
+ return new RegExp(vals.length ? `^(${vals.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$` : "^[^\\s\\S]$");
2775
+ });
2776
+ inst._zod.parse = (payload, _ctx) => {
2777
+ const input = payload.value;
2778
+ if (values.has(input)) {
2779
+ return payload;
2780
+ }
2781
+ payload.issues.push({
2782
+ code: "invalid_value",
2783
+ values: def.values,
2784
+ input,
2785
+ inst
2786
+ });
2787
+ return payload;
2788
+ };
2789
+ });
2685
2790
  var $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) => {
2686
2791
  $ZodType.init(inst, def);
2687
2792
  inst._zod.optin = "optional";
@@ -4587,6 +4692,45 @@ var enumProcessor = (schema, _ctx, json, _params) => {
4587
4692
  json.type = "string";
4588
4693
  json.enum = values;
4589
4694
  };
4695
+ var literalProcessor = (schema, ctx, json, params) => {
4696
+ const def = schema._zod.def;
4697
+ if (def.values.length === 0) {
4698
+ json.not = {};
4699
+ return;
4700
+ }
4701
+ const vals = [];
4702
+ for (const val of def.values) {
4703
+ if (val === undefined) {
4704
+ if (handleUnrepresentable(schema, ctx, json, params, "Literal `undefined` cannot be represented in JSON Schema"))
4705
+ return;
4706
+ } else if (typeof val === "bigint") {
4707
+ if (handleUnrepresentable(schema, ctx, json, params, "BigInt literals cannot be represented in JSON Schema"))
4708
+ return;
4709
+ vals.push(Number(val));
4710
+ } else {
4711
+ vals.push(val);
4712
+ }
4713
+ }
4714
+ if (vals.length === 0) {} else if (vals.length === 1) {
4715
+ const val = vals[0];
4716
+ json.type = val === null ? "null" : typeof val;
4717
+ if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
4718
+ json.enum = [val];
4719
+ } else {
4720
+ json.const = val;
4721
+ }
4722
+ } else {
4723
+ if (vals.every((v) => typeof v === "number"))
4724
+ json.type = "number";
4725
+ if (vals.every((v) => typeof v === "string"))
4726
+ json.type = "string";
4727
+ if (vals.every((v) => typeof v === "boolean"))
4728
+ json.type = "boolean";
4729
+ if (vals.every((v) => v === null))
4730
+ json.type = "null";
4731
+ json.enum = vals;
4732
+ }
4733
+ };
4590
4734
  var customProcessor = (schema, ctx, json, params) => {
4591
4735
  handleUnrepresentable(schema, ctx, json, params, "Custom types cannot be represented in JSON Schema");
4592
4736
  };
@@ -5618,6 +5762,18 @@ function union(options, params) {
5618
5762
  ...normalizeParams(params)
5619
5763
  });
5620
5764
  }
5765
+ var ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("ZodDiscriminatedUnion", (inst, def) => {
5766
+ ZodUnion.init(inst, def);
5767
+ $ZodDiscriminatedUnion.init(inst, def);
5768
+ });
5769
+ function discriminatedUnion(discriminator, options, params) {
5770
+ return new ZodDiscriminatedUnion({
5771
+ type: "union",
5772
+ options,
5773
+ discriminator,
5774
+ ...normalizeParams(params)
5775
+ });
5776
+ }
5621
5777
  var ZodIntersection = /* @__PURE__ */ $constructor("ZodIntersection", (inst, def) => {
5622
5778
  $ZodIntersection.init(inst, def);
5623
5779
  ZodType.init(inst, def);
@@ -5700,6 +5856,27 @@ function _enum(values, params) {
5700
5856
  ...normalizeParams(params)
5701
5857
  });
5702
5858
  }
5859
+ var ZodLiteral = /* @__PURE__ */ $constructor("ZodLiteral", (inst, def) => {
5860
+ $ZodLiteral.init(inst, def);
5861
+ ZodType.init(inst, def);
5862
+ inst._zod.processJSONSchema = (ctx, json, params) => literalProcessor(inst, ctx, json, params);
5863
+ inst.values = new Set(def.values);
5864
+ Object.defineProperty(inst, "value", {
5865
+ get() {
5866
+ if (def.values.length > 1) {
5867
+ throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");
5868
+ }
5869
+ return def.values[0];
5870
+ }
5871
+ });
5872
+ });
5873
+ function literal(value, params) {
5874
+ return new ZodLiteral({
5875
+ type: "literal",
5876
+ values: Array.isArray(value) ? value : [value],
5877
+ ...normalizeParams(params)
5878
+ });
5879
+ }
5703
5880
  var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
5704
5881
  _ensureDefaultMemoizer();
5705
5882
  $ZodTransform.init(inst, def);
@@ -5916,14 +6093,16 @@ var scopes = [
5916
6093
  "contacts.read",
5917
6094
  "contacts.write",
5918
6095
  "collab.read",
5919
- "collab.write"
6096
+ "collab.write",
6097
+ "agent.read"
5920
6098
  ];
5921
6099
  var scopeGroups = [
5922
6100
  { id: "account", scopes: ["account.read", "credits.read"] },
5923
6101
  { id: "mail", scopes: ["mail.read", "mail.send", "mail.search", "mail.delete"] },
5924
6102
  { id: "storage", scopes: ["storage.read", "storage.write", "storage.delete"] },
5925
6103
  { id: "collab", scopes: ["collab.read", "collab.write"] },
5926
- { id: "contacts", scopes: ["contacts.read", "contacts.write"] }
6104
+ { id: "contacts", scopes: ["contacts.read", "contacts.write"] },
6105
+ { id: "agent", scopes: ["agent.read"] }
5927
6106
  ];
5928
6107
  function expandScopes(input) {
5929
6108
  const expanded = input.flatMap((item) => {
@@ -6034,6 +6213,10 @@ var oauthBindLoginInput = object({
6034
6213
  password: string2().min(1).max(256)
6035
6214
  });
6036
6215
  var adminSettingsInput = object({ registrationOpen: boolean2() });
6216
+ var collabContentViewInput = object({
6217
+ reason: string2().trim().min(1).max(200),
6218
+ cursor: string2().trim().max(200).optional()
6219
+ });
6037
6220
  var creditBuckets = ["granted", "topup"];
6038
6221
  var creditPackStatuses = ["draft", "live", "delisted"];
6039
6222
  var adminCreditAdjustInput = object({
@@ -6140,6 +6323,7 @@ var mailSendInput = object({
6140
6323
  cc: mailRecipientList.default([]),
6141
6324
  subject: string2().trim().min(1).max(200),
6142
6325
  body: string2().min(1).max(1e5),
6326
+ bodyHtml: string2().max(300000).optional(),
6143
6327
  inReplyTo: string2().max(998).optional(),
6144
6328
  attachmentIds: array(string2().min(1).max(64)).max(10).optional()
6145
6329
  });
@@ -6147,7 +6331,8 @@ var mailDraftInput = object({
6147
6331
  to: mailRecipientList.default([]),
6148
6332
  cc: mailRecipientList.default([]),
6149
6333
  subject: string2().trim().max(200).default(""),
6150
- body: string2().max(1e5).default("")
6334
+ body: string2().max(1e5).default(""),
6335
+ bodyHtml: string2().max(1e6).optional()
6151
6336
  });
6152
6337
  var mailReadInput = object({ unread: boolean2() });
6153
6338
  var mailDevInboundInput = object({
@@ -6155,6 +6340,7 @@ var mailDevInboundInput = object({
6155
6340
  to: mailAddressSchema,
6156
6341
  subject: string2().trim().min(1).max(200),
6157
6342
  body: string2().max(1e5),
6343
+ html: string2().max(1e6).optional(),
6158
6344
  attachments: array(object({
6159
6345
  filename: string2().min(1).max(120),
6160
6346
  mimeType: string2().regex(/^[\w.+-]+\/[\w.+-]+$/).max(100),
@@ -6222,6 +6408,123 @@ var collabRecordListQuery = object({
6222
6408
  cursor: string2().trim().max(200).optional(),
6223
6409
  limit: exports_coerce.number().int().min(1).max(200).optional()
6224
6410
  });
6411
+ var agentPermissionModes = ["confirm", "auto_edit", "yolo"];
6412
+ var agentSessionKinds = ["chat", "task"];
6413
+ var agentMemoryTypes = ["preference", "contact", "project"];
6414
+ var AGENT_TITLE_MAX = 120;
6415
+ var AGENT_MEMORY_MAX = 2000;
6416
+ var AGENT_PERSONA_MAX = 2000;
6417
+ var agentToolsConfigSchema = object({
6418
+ web: boolean2().optional(),
6419
+ code: boolean2().optional(),
6420
+ workspace: boolean2().optional(),
6421
+ crab: object({
6422
+ credits: boolean2().optional(),
6423
+ mail: boolean2().optional(),
6424
+ storage: boolean2().optional(),
6425
+ collab: boolean2().optional(),
6426
+ contacts: boolean2().optional()
6427
+ }).optional()
6428
+ });
6429
+ var agentConfigUpdateInput = object({
6430
+ activeModelId: string2().trim().min(1).max(64).optional(),
6431
+ persona: string2().max(AGENT_PERSONA_MAX).optional(),
6432
+ mode: _enum(agentPermissionModes).optional(),
6433
+ tools: agentToolsConfigSchema.optional()
6434
+ });
6435
+ var AGENT_PROVIDER_LABEL_MAX = 60;
6436
+ var agentProviderInput = object({
6437
+ label: string2().trim().min(1).max(AGENT_PROVIDER_LABEL_MAX),
6438
+ baseUrl: string2().trim().min(1).max(500),
6439
+ apiKey: string2().trim().min(1).max(500)
6440
+ });
6441
+ var agentProviderModelInput = object({
6442
+ model: string2().trim().min(1).max(200),
6443
+ contextWindow: number2().int().min(1000).max(1e8).nullable().optional(),
6444
+ maxOutput: number2().int().min(100).max(1e7).nullable().optional(),
6445
+ inputText: boolean2().optional(),
6446
+ inputImage: boolean2().optional(),
6447
+ inputVideo: boolean2().optional()
6448
+ });
6449
+ var agentSessionCreateInput = object({
6450
+ kind: _enum(agentSessionKinds).default("chat"),
6451
+ title: string2().trim().max(AGENT_TITLE_MAX).optional(),
6452
+ repeatCron: string2().trim().max(100).optional(),
6453
+ notifyMail: boolean2().optional()
6454
+ }).refine((v) => v.kind === "chat" || !!v.title && v.title.length > 0, {
6455
+ message: "task requires title"
6456
+ });
6457
+ var agentSessionRenameInput = object({ title: string2().trim().min(1).max(AGENT_TITLE_MAX) });
6458
+ var agentSessionListQuery = object({
6459
+ kind: _enum(agentSessionKinds).optional(),
6460
+ limit: exports_coerce.number().int().min(1).max(200).optional()
6461
+ });
6462
+ var agentMemoryCreateInput = object({
6463
+ type: _enum(agentMemoryTypes),
6464
+ content: string2().trim().min(1).max(AGENT_MEMORY_MAX),
6465
+ sessionId: string2().max(64).optional()
6466
+ });
6467
+ var agentTraceStatuses = ["running", "ok", "error", "pending", "approved", "rejected"];
6468
+ var agentPlanItemStates = ["done", "current", "todo"];
6469
+ var agentTraceCommon = {
6470
+ id: string2().min(1).max(64),
6471
+ status: _enum(agentTraceStatuses),
6472
+ round: number2().int().min(1),
6473
+ title: string2().max(200),
6474
+ durationMs: number2().int().min(0).optional()
6475
+ };
6476
+ var agentPlanTraceSchema = object({
6477
+ ...agentTraceCommon,
6478
+ kind: literal("plan"),
6479
+ items: array(object({ text: string2().max(500), state: _enum(agentPlanItemStates) })).max(50)
6480
+ });
6481
+ var agentWebTraceSchema = object({
6482
+ ...agentTraceCommon,
6483
+ kind: literal("web"),
6484
+ url: string2().max(2000),
6485
+ summary: string2().max(8000)
6486
+ });
6487
+ var agentArtifactSchema = object({
6488
+ name: string2().max(200),
6489
+ mime: string2().max(100),
6490
+ path: string2().max(500),
6491
+ rows: number2().int().min(0).optional()
6492
+ });
6493
+ var agentCodeTraceSchema = object({
6494
+ ...agentTraceCommon,
6495
+ kind: literal("code"),
6496
+ code: string2().max(50000).optional(),
6497
+ result: string2().max(20000).optional(),
6498
+ artifact: agentArtifactSchema.optional()
6499
+ });
6500
+ var agentCrabTraceSchema = object({
6501
+ ...agentTraceCommon,
6502
+ kind: literal("crab"),
6503
+ command: string2().max(2000),
6504
+ response: string2().max(8000).optional()
6505
+ });
6506
+ var agentApprovalTraceSchema = object({
6507
+ ...agentTraceCommon,
6508
+ kind: literal("approval"),
6509
+ tool: string2().max(64),
6510
+ action: string2().max(200),
6511
+ detail: string2().max(8000)
6512
+ });
6513
+ var agentTraceSchema = discriminatedUnion("kind", [
6514
+ agentPlanTraceSchema,
6515
+ agentWebTraceSchema,
6516
+ agentCodeTraceSchema,
6517
+ agentCrabTraceSchema,
6518
+ agentApprovalTraceSchema
6519
+ ]);
6520
+ var agentMemoryChipSchema = object({
6521
+ text: string2().max(200),
6522
+ memoryId: string2().max(64).optional()
6523
+ });
6524
+ var agentCompressionChipSchema = object({
6525
+ foldedRounds: number2().int().min(1),
6526
+ summary: string2().max(4000)
6527
+ });
6225
6528
 
6226
6529
  // ../../packages/cloudkit/src/crypto.ts
6227
6530
  var enc = new TextEncoder;
@@ -6548,9 +6851,6 @@ function formatTime(ms) {
6548
6851
  function fmtCredits(cents) {
6549
6852
  return (cents / 100).toLocaleString("en-US");
6550
6853
  }
6551
- function fmtUsd(cents) {
6552
- return `$${cents / 100}`;
6553
- }
6554
6854
  var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6555
6855
  发布物经 npm 分发:npx @crabcloud/cli <command>(全局安装后短名 crab 等价)
6556
6856
 
@@ -6574,7 +6874,10 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6574
6874
  mail list [--folder inbox|sent|drafts|archive|trash] [--unread] [--limit N] [--json]
6575
6875
  列出邮件(cursor 分页契约与 Web 工作台一致)
6576
6876
  mail read <id> [--json] 读取邮件正文(自动置已读)
6877
+ mail unread <id> 标记为未读
6577
6878
  mail search <query> [--json] 全文搜索(主题/发件人/收件人/正文)
6879
+ mail folders [--json] 邮箱概览(各夹总数/未读 + 本月收发统计)
6880
+ mail activity [--json] 邮箱操作流(谁读过/谁代发,最近 50 条)
6578
6881
  mail send --to a@x.cc|联系人名,b@y.cn [--cc c@z.io] --subject "..." --body "..." | --body-file <path>
6579
6882
  [--attach <云盘id|文件名>,...] [--in-reply-to <message-id>]
6580
6883
  回信线程锚点(原信 RFC Message-ID,crab mail read 可见)
@@ -6582,7 +6885,7 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6582
6885
  命中采用,多候选报错列出);--attach 引用云盘已有对象
6583
6886
  (id 或文件名精确唯一命中,未绑信对象,≤10 个,大文件
6584
6887
  自动转下载链接);平台内(@crabcloud.cc)免费,外部出站
6585
- 2 积分/封
6888
+ 0.02 积分/封
6586
6889
  mail archive|trash|delete <id> 移夹 / 废纸篓 / 彻底删除(delete 不可恢复)
6587
6890
  contacts list [query] [--offset N] [--json] 列出联系人(地址前缀/显示名匹配)
6588
6891
  contacts add <address> [--name 名] [--note 备注] [--json]
@@ -6591,13 +6894,15 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6591
6894
  contacts invite [--reset] [--json] 我的邀请码、链接与统计(--reset 重置,旧链接失效)
6592
6895
  contacts invitees [--offset N] [--json]
6593
6896
  我邀请的人(直接下级 + 间接计数)
6594
- storage ls [--q 关键字] [--source files|media|mail] [--category <id|none>]
6897
+ storage ls [--q 关键字] [--source files|media|mail|collab|agent] [--category <id|none>]
6595
6898
  [--type doc|image|video|archive|code] [--tag a,b] [--trash]
6596
- [--shared] [--recent] [--message-id <id>] [--limit N] [--offset N] [--json]
6597
- 列出云盘对象(跨来源统一视图,能力与 Web 云盘一致)
6899
+ [--shared] [--recent] [--encrypted only|plain] [--message-id <id>]
6900
+ [--limit N] [--offset N] [--json]
6901
+ 列出云盘对象(跨来源统一视图,能力与 Web 云盘一致;
6902
+ --encrypted only=仅保险箱、plain=排除保险箱)
6598
6903
  storage upload <file...> [--json] 上传一个或多个文件(直传优先,自动回退中转);
6599
- files 域默认端到端加密,口令 --passphrase /
6600
- CRAB_PASSPHRASE 提供,终端内运行时交互输入
6904
+ 默认明文上传(2026-10 基础服务化);--passphrase /
6905
+ CRAB_PASSPHRASE 提供时端到端加密(保险箱文件)
6601
6906
  storage download <id|文件名> [-o 输出路径]
6602
6907
  下载对象内容(默认按原文件名存到当前目录;
6603
6908
  加密对象需口令,终端内运行时交互输入)
@@ -6610,7 +6915,7 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6610
6915
  公开 / 收回公开(公开时打印公开直链)
6611
6916
  storage rm|restore|purge <id|文件名>
6612
6917
  回收站 / 恢复 / 彻底删除(purge 不可恢复)
6613
- storage quota [--json] 存储水位(统一容量账:文件/素材/邮件附件一本账)
6918
+ storage quota [--json] 存储水位(统一一本账:五来源分项 + 保险箱计数)
6614
6919
  storage share <id|文件名> [--expires 7|30|90|forever] [--passcode 提取码]
6615
6920
  创建分享链接(加密文件链接自带解密钥匙 #fragment)
6616
6921
  storage key [--remember|--forget]
@@ -6625,10 +6930,11 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6625
6930
  分类重命名 / 改色
6626
6931
  storage rmcat <id|分类名> 删除分类(其中文件回落未分类)
6627
6932
  storage tags [--json] 标签汇总
6933
+ storage activity [--json] 云盘操作流(读取/上传/删除/建分享,最近 50 条)
6628
6934
  collab spaces [--json] 列出我所在的协作空间(任务/成员计数与限额)
6629
6935
  collab space create <名称> [--allocation-mb N] [--json]
6630
6936
  创建独立协作空间(消耗 5 积分;默认自分配 500MB
6631
- 存储占订阅总池,可随时调)
6937
+ 存储占订阅总池;后续调整在网页端空间设置)
6632
6938
  collab tasks [--space <arg>] [--status open|done] [--q 关键字]
6633
6939
  [--limit N] [--cursor <cursor>] [--json]
6634
6940
  列出任务(含摘要/记录数;--q 搜标题与记录正文)
@@ -6859,10 +7165,10 @@ async function creditsPricing() {
6859
7165
  console.log("暂无动作计价项。");
6860
7166
  return 0;
6861
7167
  }
6862
- console.log("动作计价(1 积分 = $1,价目为 $ 口径):");
7168
+ console.log("动作计价(1 积分 = $1,价目为积分数):");
6863
7169
  for (const item of actions) {
6864
7170
  const label = CREDIT_ACTION_LABELS[item.action] ?? item.action;
6865
- console.log(` ${item.action.padEnd(24)} ${label} ${fmtUsd(item.creditCents)}`);
7171
+ console.log(` ${item.action.padEnd(24)} ${label} ${fmtCredits(item.creditCents)} 积分`);
6866
7172
  }
6867
7173
  return 0;
6868
7174
  }
@@ -7114,8 +7420,8 @@ async function mailSend() {
7114
7420
  const hit = await resolveStorageObject(credentials, token);
7115
7421
  if (typeof hit === "number")
7116
7422
  return hit;
7117
- if (typeof hit === "object" && hit.isEncrypted) {
7118
- console.error(`「${token}」是端到端加密文件,暂不能作为邮件附件发送(服务端无法解密)。`);
7423
+ if (typeof hit === "object" && hit.isEncrypted && hit.wrappedDataKey?.startsWith("u1.")) {
7424
+ console.error(`「${token}」是保险箱文件(端到端加密),不能作为邮件附件发送。`);
7119
7425
  return EXIT_USAGE;
7120
7426
  }
7121
7427
  const id = typeof hit === "object" ? hit.id : hit;
@@ -7140,7 +7446,7 @@ async function mailSend() {
7140
7446
  if (attachmentIds.length > 0)
7141
7447
  console.log(` 附件 ${attachmentIds.length} 个(云盘引用,不复制)`);
7142
7448
  if (result.chargedCredits > 0)
7143
- console.log(` 消耗:${fmtUsd(result.chargedCredits)}`);
7449
+ console.log(` 消耗:${fmtCredits(result.chargedCredits)} 积分`);
7144
7450
  if (result.failedExternal > 0)
7145
7451
  console.log(` ⚠ ${result.failedExternal} 个外部收件人发送失败,对应积分已退回`);
7146
7452
  return 0;
@@ -7161,6 +7467,72 @@ async function mailMove(action, id) {
7161
7467
  console.log(`✓ ${action === "archive" ? "已归档" : action === "trash" ? "已移入废纸篓" : "已彻底删除"}(${id})`);
7162
7468
  return 0;
7163
7469
  }
7470
+ async function mailUnread(id) {
7471
+ if (!id) {
7472
+ console.error("用法:crab mail unread <id>(id 见 crab mail list)");
7473
+ return EXIT_USAGE;
7474
+ }
7475
+ const credentials = requireCredentials();
7476
+ if (typeof credentials === "number")
7477
+ return credentials;
7478
+ const res = await api2(credentials.apiBase, `/v1/mail/messages/${encodeURIComponent(id)}/read`, {
7479
+ method: "POST",
7480
+ headers: authHeaders(credentials),
7481
+ body: JSON.stringify({ unread: true })
7482
+ });
7483
+ if (res.status !== 200) {
7484
+ console.error(errorMessage(res.body, "标记未读失败。"));
7485
+ return exitCodeForApiError(res.body);
7486
+ }
7487
+ console.log(`✓ 已标为未读(${id})。`);
7488
+ return 0;
7489
+ }
7490
+ var MAILBOX_FOLDER_LABELS = { inbox: "收件箱", sent: "已发送", drafts: "草稿", archive: "归档", trash: "废纸篓" };
7491
+ async function mailFoldersOverview() {
7492
+ const credentials = requireCredentials();
7493
+ if (typeof credentials === "number")
7494
+ return credentials;
7495
+ const res = await api2(credentials.apiBase, "/v1/mail/folders", { headers: authHeaders(credentials) });
7496
+ if (res.status !== 200) {
7497
+ console.error(errorMessage(res.body, "获取邮箱概览失败。"));
7498
+ return exitCodeForApiError(res.body);
7499
+ }
7500
+ const overview = res.body.data;
7501
+ if (hasFlag("--json")) {
7502
+ console.log(JSON.stringify(overview, null, 2));
7503
+ return 0;
7504
+ }
7505
+ console.log(overview.address);
7506
+ for (const folder of overview.folders) {
7507
+ console.log(` ${MAILBOX_FOLDER_LABELS[folder.folder] ?? folder.folder} ${folder.total} 封${folder.unread > 0 ? `(未读 ${folder.unread})` : ""}`);
7508
+ }
7509
+ console.log(`本月:收 ${overview.month.received} · 发 ${overview.month.sent}(外部出站 ${overview.month.outbound})`);
7510
+ return 0;
7511
+ }
7512
+ var MAIL_ACTIVITY_LABELS = { read: "读取", drafted: "撰写", sent: "发送" };
7513
+ async function mailActivity() {
7514
+ const credentials = requireCredentials();
7515
+ if (typeof credentials === "number")
7516
+ return credentials;
7517
+ const res = await api2(credentials.apiBase, "/v1/mail/agent-activity", { headers: authHeaders(credentials) });
7518
+ if (res.status !== 200) {
7519
+ console.error(errorMessage(res.body, "获取邮箱活动失败。"));
7520
+ return exitCodeForApiError(res.body);
7521
+ }
7522
+ const activity = res.body.data.activity;
7523
+ if (hasFlag("--json")) {
7524
+ console.log(JSON.stringify(activity, null, 2));
7525
+ return 0;
7526
+ }
7527
+ if (activity.length === 0) {
7528
+ console.log("还没有邮箱操作记录。");
7529
+ return 0;
7530
+ }
7531
+ for (const entry of activity) {
7532
+ console.log(`${formatTime(entry.createdAt)} ${(MAIL_ACTIVITY_LABELS[entry.action] ?? entry.action).padEnd(4)} ${entry.actorType === "agent" ? "agent" : "user "} ${entry.actorLabel ?? "—"} ${entry.subject || "(无主题)"}`);
7533
+ }
7534
+ return 0;
7535
+ }
7164
7536
  function contactDisplayName(contact) {
7165
7537
  return contact.displayName ?? contact.resolvedName ?? contact.address;
7166
7538
  }
@@ -7272,10 +7644,7 @@ async function contactsInvite(reset) {
7272
7644
  const credentials = requireCredentials();
7273
7645
  if (typeof credentials === "number")
7274
7646
  return credentials;
7275
- const res = await api2(credentials.apiBase, "/v1/contacts/invite/reset", {
7276
- method: reset ? "POST" : "GET",
7277
- headers: authHeaders(credentials)
7278
- });
7647
+ const res = await api2(credentials.apiBase, reset ? "/v1/contacts/invite/reset" : "/v1/contacts/invite", { method: reset ? "POST" : "GET", headers: authHeaders(credentials) });
7279
7648
  if (res.status !== 200) {
7280
7649
  console.error(errorMessage(res.body, "获取邀请链接失败。"));
7281
7650
  return exitCodeForApiError(res.body);
@@ -7321,7 +7690,7 @@ async function contactsInvitees() {
7321
7690
  console.log(`(还有更多:contacts invitees --offset ${page.nextOffset})`);
7322
7691
  return 0;
7323
7692
  }
7324
- var STORAGE_SOURCES = ["files", "media", "mail"];
7693
+ var STORAGE_SOURCES = ["files", "media", "mail", "collab", "agent"];
7325
7694
  var STORAGE_TYPES = ["doc", "image", "video", "archive", "code"];
7326
7695
  var MIME_BY_EXT = {
7327
7696
  png: "image/png",
@@ -7358,9 +7727,10 @@ function formatBytesCli(n) {
7358
7727
  return `${n} B`;
7359
7728
  }
7360
7729
  function printStorageRow(o) {
7730
+ const encFlag = o.isEncrypted ? o.wrappedDataKey?.startsWith("u1.") ? "保险箱" : "平台加密" : null;
7361
7731
  const flags = [
7362
- o.source !== "files" ? o.source === "media" ? "素材" : "邮件附件" : null,
7363
- o.isEncrypted ? "加密" : null,
7732
+ o.source !== "files" ? o.source === "media" ? "素材" : o.source === "mail" ? "邮件附件" : o.source === "collab" ? "协作" : "Agent 产物" : null,
7733
+ encFlag,
7364
7734
  o.isPublic ? "公开" : null
7365
7735
  ].filter(Boolean);
7366
7736
  console.log(`${o.id} ${o.filename} ${formatBytesCli(o.sizeBytes)} ${formatTime(o.createdAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
@@ -7430,6 +7800,15 @@ async function storageList() {
7430
7800
  const tags = readFlag("--tag");
7431
7801
  if (tags)
7432
7802
  params.set("tags", tags);
7803
+ const encrypted = readFlag("--encrypted");
7804
+ if (encrypted) {
7805
+ if (encrypted !== "only" && encrypted !== "plain") {
7806
+ console.error(`未知 encrypted 过滤:${encrypted}
7807
+ 可用:only(仅保险箱)、plain(排除保险箱)`);
7808
+ return EXIT_USAGE;
7809
+ }
7810
+ params.set("encrypted", encrypted);
7811
+ }
7433
7812
  if (hasFlag("--trash"))
7434
7813
  params.set("trash", "1");
7435
7814
  if (hasFlag("--shared"))
@@ -7688,7 +8067,7 @@ async function driveMasterKey(credentials) {
7688
8067
  return raw;
7689
8068
  return importAesKey(raw);
7690
8069
  }
7691
- async function storageUploadOne(credentials, filePath) {
8070
+ async function storageUploadOne(credentials, filePath, encrypt) {
7692
8071
  let buf;
7693
8072
  try {
7694
8073
  buf = readFileSync2(filePath);
@@ -7698,22 +8077,26 @@ async function storageUploadOne(credentials, filePath) {
7698
8077
  }
7699
8078
  const filename = basename(filePath);
7700
8079
  const mimeType = mimeFor(filename);
7701
- const master = await driveMasterKey(credentials);
7702
- if (typeof master === "number")
7703
- return master;
7704
- const dataKey = randomKeyBytes(32);
7705
- const sealed = await aeadEncrypt(await importAesKey(dataKey), new Uint8Array(buf), DATA_AAD);
7706
- const cipher = new Uint8Array([...sealed.iv, ...sealed.ciphertext]);
7707
- const wrapped = await aeadEncrypt(master, dataKey, DATA_AAD);
7708
- const wrappedDataKey = `u1.${sealToB64url({ iv: wrapped.iv, ciphertext: wrapped.ciphertext })}`;
8080
+ let body = new Uint8Array(buf);
8081
+ let wrappedDataKey = null;
8082
+ if (encrypt) {
8083
+ const master = await driveMasterKey(credentials);
8084
+ if (typeof master === "number")
8085
+ return master;
8086
+ const dataKey = randomKeyBytes(32);
8087
+ const sealed = await aeadEncrypt(await importAesKey(dataKey), body, DATA_AAD);
8088
+ body = new Uint8Array([...sealed.iv, ...sealed.ciphertext]);
8089
+ const wrapped = await aeadEncrypt(master, dataKey, DATA_AAD);
8090
+ wrappedDataKey = `u1.${sealToB64url({ iv: wrapped.iv, ciphertext: wrapped.ciphertext })}`;
8091
+ }
7709
8092
  const presign = await api2(credentials.apiBase, "/v1/storage/presign", {
7710
8093
  method: "POST",
7711
8094
  headers: authHeaders(credentials),
7712
- body: JSON.stringify({ filename, mimeType, sizeBytes: cipher.byteLength, enc: true })
8095
+ body: JSON.stringify({ filename, mimeType, sizeBytes: body.byteLength, enc: wrappedDataKey !== null })
7713
8096
  });
7714
8097
  if (presign.status === 200 || presign.status === 201) {
7715
8098
  const { fileId, uploadUrl } = presign.body.data;
7716
- const put = await fetch(uploadUrl, { method: "PUT", body: cipher, headers: { "content-type": mimeType } });
8099
+ const put = await fetch(uploadUrl, { method: "PUT", body, headers: { "content-type": mimeType } });
7717
8100
  if (!put.ok) {
7718
8101
  console.error(`直传失败(${put.status}):${filename}`);
7719
8102
  return 1;
@@ -7721,7 +8104,7 @@ async function storageUploadOne(credentials, filePath) {
7721
8104
  const complete = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(fileId)}/complete`, {
7722
8105
  method: "POST",
7723
8106
  headers: authHeaders(credentials),
7724
- body: JSON.stringify({ isEncrypted: true, wrappedDataKey })
8107
+ body: JSON.stringify(wrappedDataKey ? { isEncrypted: true, wrappedDataKey } : {})
7725
8108
  });
7726
8109
  if (complete.status !== 200 && complete.status !== 201) {
7727
8110
  console.error(errorMessage(complete.body, `记账失败:${filename}`));
@@ -7731,28 +8114,30 @@ async function storageUploadOne(credentials, filePath) {
7731
8114
  if (hasFlag("--json"))
7732
8115
  console.log(JSON.stringify(object, null, 2));
7733
8116
  else
7734
- console.log(`✓ 已上传(已加密):${object.filename}(id ${object.id})`);
8117
+ console.log(`✓ 已上传${wrappedDataKey ? "(已加密)" : ""}:${object.filename}(id ${object.id})`);
7735
8118
  return 0;
7736
8119
  }
7737
8120
  if (presign.body?.error?.code === "presign_unavailable") {
7738
8121
  const form = new FormData;
7739
- form.append("file", new Blob([cipher], { type: mimeType }), filename);
7740
- form.append("enc", "1");
7741
- form.append("wrappedDataKey", wrappedDataKey);
8122
+ form.append("file", new Blob([body], { type: mimeType }), filename);
8123
+ if (wrappedDataKey) {
8124
+ form.append("enc", "1");
8125
+ form.append("wrappedDataKey", wrappedDataKey);
8126
+ }
7742
8127
  const res = await fetch(`${credentials.apiBase}/v1/storage`, {
7743
8128
  method: "POST",
7744
8129
  headers: authHeaders(credentials),
7745
8130
  body: form
7746
8131
  });
7747
- const body = await res.json().catch(() => ({}));
8132
+ const body2 = await res.json().catch(() => ({}));
7748
8133
  if (res.status !== 200 && res.status !== 201) {
7749
- console.error(body.error?.message ?? `上传失败:${filename}`);
7750
- return exitCodeForApiError(body);
8134
+ console.error(body2.error?.message ?? `上传失败:${filename}`);
8135
+ return exitCodeForApiError(body2);
7751
8136
  }
7752
8137
  if (hasFlag("--json"))
7753
- console.log(JSON.stringify(body.data, null, 2));
8138
+ console.log(JSON.stringify(body2.data, null, 2));
7754
8139
  else
7755
- console.log(`✓ 已上传(已加密):${body.data?.filename}(id ${body.data?.id})`);
8140
+ console.log(`✓ 已上传${wrappedDataKey ? "(已加密)" : ""}:${body2.data?.filename}(id ${body2.data?.id})`);
7756
8141
  return 0;
7757
8142
  }
7758
8143
  console.error(errorMessage(presign.body, `上传失败:${filename}`));
@@ -7760,16 +8145,21 @@ async function storageUploadOne(credentials, filePath) {
7760
8145
  }
7761
8146
  async function storageUpload(paths) {
7762
8147
  const files = [];
8148
+ let encrypt = false;
7763
8149
  for (let i = 0;i < paths.length; i++) {
7764
8150
  if (paths[i].startsWith("--")) {
7765
- if (paths[i] === "--passphrase")
8151
+ if (paths[i] === "--passphrase") {
8152
+ encrypt = true;
7766
8153
  i++;
8154
+ }
7767
8155
  continue;
7768
8156
  }
7769
8157
  files.push(paths[i]);
7770
8158
  }
8159
+ if (!encrypt && process.env.CRAB_PASSPHRASE)
8160
+ encrypt = true;
7771
8161
  if (files.length === 0) {
7772
- console.error("用法:crab storage upload <file...> [--passphrase 口令] [--json]");
8162
+ console.error("用法:crab storage upload <file...> [--passphrase 口令] [--json](缺省明文上传;带口令则端到端加密)");
7773
8163
  return EXIT_USAGE;
7774
8164
  }
7775
8165
  const credentials = requireCredentials();
@@ -7777,7 +8167,7 @@ async function storageUpload(paths) {
7777
8167
  return credentials;
7778
8168
  let code = 0;
7779
8169
  for (const file of files) {
7780
- const result = await storageUploadOne(credentials, file);
8170
+ const result = await storageUploadOne(credentials, file, encrypt);
7781
8171
  if (result !== 0 && code === 0)
7782
8172
  code = result;
7783
8173
  if (result === EXIT_AUTH || result === EXIT_SCOPE)
@@ -8034,7 +8424,8 @@ async function storageShares(token) {
8034
8424
  }
8035
8425
  for (const s of rows) {
8036
8426
  const state = s.revokedAt ? "已撤销" : s.expiresAt !== null && s.expiresAt <= Date.now() ? "已过期" : "生效中";
8037
- console.log(`${s.id} …${s.token.slice(-12)} ${state} 下载 ${s.downloadCount} 次 ${formatTime(s.createdAt)}${s.hasPasscode ? " [提取码]" : ""}`);
8427
+ const srcLabel = { drive: "云盘", mail: "邮件引用", collab: "协作引用", api: "API" }[s.source ?? "drive"];
8428
+ console.log(`${s.id} …${s.token.slice(-12)} ${state} ${srcLabel} 下载 ${s.downloadCount} 次 ${formatTime(s.createdAt)}${s.hasPasscode ? " [提取码]" : ""}`);
8038
8429
  }
8039
8430
  return 0;
8040
8431
  }
@@ -8072,9 +8463,10 @@ async function storageQuota() {
8072
8463
  return 0;
8073
8464
  }
8074
8465
  const pct = quota.quotaBytes > 0 ? (quota.usedBytes / quota.quotaBytes * 100).toFixed(1) : "0";
8466
+ const libraryCount = (quota.counts.all ?? 0) - (quota.counts.vault ?? 0);
8075
8467
  console.log(`已用 ${formatBytesCli(quota.usedBytes)} / ${formatBytesCli(quota.quotaBytes)}(${pct}%)`);
8076
- console.log(` 文件 ${formatBytesCli(quota.bySource.files)} · 素材 ${formatBytesCli(quota.bySource.media)} · 邮件附件 ${formatBytesCli(quota.bySource.mail)} · 回收站 ${formatBytesCli(quota.trashBytes)}`);
8077
- console.log(` 对象数:全部 ${quota.counts.all ?? 0} · 素材 ${quota.counts.media ?? 0} · 邮件附件 ${quota.counts.mail ?? 0} · 共享 ${quota.counts.shared ?? 0} · 回收站 ${quota.counts.trash ?? 0}`);
8468
+ console.log(` 文件 ${formatBytesCli(quota.bySource.files)} · 素材 ${formatBytesCli(quota.bySource.media)} · ` + `邮件附件 ${formatBytesCli(quota.bySource.mail)} · 协作 ${formatBytesCli(quota.bySource.collab)} · ` + `Agent ${formatBytesCli(quota.bySource.agent)} · 回收站 ${formatBytesCli(quota.trashBytes)}`);
8469
+ console.log(` 对象数:全部 ${quota.counts.all ?? 0} · 素材库 ${libraryCount}(保险箱 ${quota.counts.vault ?? 0}) · ` + `共享 ${quota.counts.shared ?? 0} · 回收站 ${quota.counts.trash ?? 0}`);
8078
8470
  return 0;
8079
8471
  }
8080
8472
  async function storageCategories() {
@@ -8230,6 +8622,35 @@ async function storageTags() {
8230
8622
  console.log(`${row.tag} (${row.count})`);
8231
8623
  return 0;
8232
8624
  }
8625
+ var STORAGE_ACTIVITY_LABELS = {
8626
+ "object.read": "读取",
8627
+ "object.uploaded": "上传",
8628
+ "object.deleted": "删除",
8629
+ "share.created": "建分享"
8630
+ };
8631
+ async function storageActivity() {
8632
+ const credentials = requireCredentials();
8633
+ if (typeof credentials === "number")
8634
+ return credentials;
8635
+ const res = await api2(credentials.apiBase, "/v1/storage/agent-activity", { headers: authHeaders(credentials) });
8636
+ if (res.status !== 200) {
8637
+ console.error(errorMessage(res.body, "获取云盘活动失败。"));
8638
+ return exitCodeForApiError(res.body);
8639
+ }
8640
+ const activity = res.body.data.activity;
8641
+ if (hasFlag("--json")) {
8642
+ console.log(JSON.stringify(activity, null, 2));
8643
+ return 0;
8644
+ }
8645
+ if (activity.length === 0) {
8646
+ console.log("还没有云盘操作记录。");
8647
+ return 0;
8648
+ }
8649
+ for (const entry of activity) {
8650
+ console.log(`${formatTime(entry.createdAt)} ${(STORAGE_ACTIVITY_LABELS[entry.action] ?? entry.action).padEnd(4)} ${entry.actorType === "agent" ? "agent" : "user "} ${entry.actorLabel ?? "—"} ${entry.objectName || entry.objectId}`);
8651
+ }
8652
+ return 0;
8653
+ }
8233
8654
  var COLLAB_TASK_STATUSES = ["open", "done"];
8234
8655
  function collabSpaceToken(space) {
8235
8656
  return space.kind === "default" ? "default" : `space:${space.name}`;
@@ -8306,7 +8727,7 @@ async function collabSpacesList() {
8306
8727
  }
8307
8728
  for (const s of data.spaces) {
8308
8729
  const storage = s.kind === "independent" ? ` · 存储 ${formatBytesCli(s.storageUsedBytes)}/${formatBytesCli(s.storageAllocatedBytes)}` : "";
8309
- console.log(`${collabSpaceToken(s)} ${s.status === "archived" ? "[已归档·只读] " : ""}任务 ${s.taskCount}/${s.limits.taskLimit} · 受邀成员 ${s.memberCount}/${s.limits.invitedMemberLimit}${storage}`);
8730
+ console.log(`${collabSpaceToken(s)} ${s.status === "archived" ? "[已归档·只读] " : ""}任务 ${s.taskCount}/${s.limits.taskLimit} · 成员 ${s.memberCount}/${s.limits.memberLimit}${storage}`);
8310
8731
  console.log(` id ${s.id}`);
8311
8732
  }
8312
8733
  console.log(`独立空间名额:${data.independentUsed}/${data.independentLimit}(创建消耗 5 积分:crab collab space create <名称>)`);
@@ -8339,7 +8760,7 @@ async function collabSpaceCreate(name) {
8339
8760
  return 0;
8340
8761
  }
8341
8762
  console.log(`✓ 独立协作空间已创建(id ${data.spaceId})`);
8342
- console.log(` 消耗:${fmtUsd(data.chargedCredits)} · --space space:${name} 即可指向它`);
8763
+ console.log(` 消耗:${fmtCredits(data.chargedCredits)} 积分 · --space space:${name} 即可指向它`);
8343
8764
  return 0;
8344
8765
  }
8345
8766
  async function collabTasksList() {
@@ -8782,14 +9203,20 @@ switch (command) {
8782
9203
  process.exit(await mailList());
8783
9204
  if (sub === "read")
8784
9205
  process.exit(await mailRead(args[2]));
9206
+ if (sub === "unread")
9207
+ process.exit(await mailUnread(args[2]));
8785
9208
  if (sub === "search")
8786
9209
  process.exit(await mailSearch(args.slice(2).join(" ") || undefined));
8787
9210
  if (sub === "send")
8788
9211
  process.exit(await mailSend());
8789
9212
  if (sub === "archive" || sub === "trash" || sub === "delete")
8790
9213
  process.exit(await mailMove(sub, args[2]));
9214
+ if (sub === "folders")
9215
+ process.exit(await mailFoldersOverview());
9216
+ if (sub === "activity")
9217
+ process.exit(await mailActivity());
8791
9218
  console.error(sub ? `未知子命令:mail ${sub}
8792
- ` : `缺少子命令:mail list | read | search | send | archive | trash | delete
9219
+ ` : `缺少子命令:mail list | read | unread | search | send | archive | trash | delete | folders | activity
8793
9220
  `);
8794
9221
  console.log(usage);
8795
9222
  process.exit(EXIT_USAGE);
@@ -8852,8 +9279,24 @@ switch (command) {
8852
9279
  process.exit(await storageUnshare(args[2]));
8853
9280
  if (sub === "key")
8854
9281
  process.exit(await storageKeyCmd());
9282
+ if (sub === "activity")
9283
+ process.exit(await storageActivity());
8855
9284
  console.error(sub ? `未知子命令:storage ${sub}
8856
- ` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | rencat | rmcat | tags | share | shares | unshare | key
9285
+ ` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | rencat | rmcat | tags | share | shares | unshare | key | activity
9286
+ `);
9287
+ console.log(usage);
9288
+ process.exit(EXIT_USAGE);
9289
+ }
9290
+ case "agent": {
9291
+ const sub = args[1];
9292
+ if (sub === "status")
9293
+ process.exit(await agentStatus());
9294
+ if (sub === "sessions")
9295
+ process.exit(await agentSessions());
9296
+ if (sub === "approvals")
9297
+ process.exit(await agentApprovals());
9298
+ console.error(sub ? `未知子命令:agent ${sub}
9299
+ ` : `缺少子命令:agent status | sessions | approvals
8857
9300
  `);
8858
9301
  console.log(usage);
8859
9302
  process.exit(EXIT_USAGE);
@@ -8928,3 +9371,73 @@ switch (command) {
8928
9371
  console.log(usage);
8929
9372
  process.exit(EXIT_USAGE);
8930
9373
  }
9374
+ async function agentStatus() {
9375
+ const credentials = requireCredentials();
9376
+ if (typeof credentials === "number")
9377
+ return credentials;
9378
+ const [sessionsRes, approvalsRes] = await Promise.all([
9379
+ api2(credentials.apiBase, "/v1/agent/sessions", { headers: authHeaders(credentials) }),
9380
+ api2(credentials.apiBase, "/v1/agent/approvals", { headers: authHeaders(credentials) })
9381
+ ]);
9382
+ if (sessionsRes.status !== 200 || approvalsRes.status !== 200) {
9383
+ console.error(errorMessage(sessionsRes.body ?? approvalsRes.body, "获取 Agent 状态失败。"));
9384
+ if (sessionsRes.body?.error?.code === "scope_denied") {
9385
+ console.error("提示:需要 agent.read scope——运行 crab login --scopes agent 重新授权。");
9386
+ }
9387
+ return 1;
9388
+ }
9389
+ const sessions = sessionsRes.body.data?.sessions ?? [];
9390
+ const approvals = approvalsRes.body.data?.approvals ?? [];
9391
+ const running = sessions.filter((x) => x.status === "running").length;
9392
+ const pending = approvals.filter((x) => x.status === "pending").length;
9393
+ if (hasFlag("--json")) {
9394
+ console.log(JSON.stringify({ sessions: sessions.length, running, pendingApprovals: pending }, null, 2));
9395
+ return 0;
9396
+ }
9397
+ console.log(`个人云 Agent:会话 ${sessions.length} · 运行中 ${running} · 待审批 ${pending}`);
9398
+ return 0;
9399
+ }
9400
+ async function agentSessions() {
9401
+ const credentials = requireCredentials();
9402
+ if (typeof credentials === "number")
9403
+ return credentials;
9404
+ const res = await api2(credentials.apiBase, "/v1/agent/sessions", { headers: authHeaders(credentials) });
9405
+ if (res.status !== 200) {
9406
+ console.error(errorMessage(res.body, "获取会话列表失败。"));
9407
+ return 1;
9408
+ }
9409
+ const sessions = res.body.data?.sessions ?? [];
9410
+ if (hasFlag("--json")) {
9411
+ console.log(JSON.stringify(sessions, null, 2));
9412
+ return 0;
9413
+ }
9414
+ for (const x of sessions) {
9415
+ console.log(`${x.kind === "task" ? "[任务]" : "[对话]"} ${x.title} ${x.status}${x.repeatCron ? ` cron ${x.repeatCron}` : ""} ${new Date(x.updatedAt).toLocaleString("zh-CN")}`);
9416
+ }
9417
+ console.log(`共 ${sessions.length} 个会话`);
9418
+ return 0;
9419
+ }
9420
+ async function agentApprovals() {
9421
+ const credentials = requireCredentials();
9422
+ if (typeof credentials === "number")
9423
+ return credentials;
9424
+ const res = await api2(credentials.apiBase, "/v1/agent/approvals", { headers: authHeaders(credentials) });
9425
+ if (res.status !== 200) {
9426
+ console.error(errorMessage(res.body, "获取审批队列失败。"));
9427
+ return 1;
9428
+ }
9429
+ const approvals = res.body.data?.approvals ?? [];
9430
+ if (hasFlag("--json")) {
9431
+ console.log(JSON.stringify(approvals, null, 2));
9432
+ return 0;
9433
+ }
9434
+ if (approvals.length === 0) {
9435
+ console.log("没有待处理或已过期的审批。");
9436
+ return 0;
9437
+ }
9438
+ for (const x of approvals) {
9439
+ console.log(`${x.status === "pending" ? "[待审批]" : "[已超时]"} ${x.tool} ${x.sessionTitle}`);
9440
+ console.log(` ${x.summary.slice(0, 160)}`);
9441
+ }
9442
+ return 0;
9443
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: crabcloud
3
- description: Manage the user's Crab Cloud personal cloud account and agent tokens through the `crab` CLI — check identity/subscription/credits, list and revoke authorized agent tokens, and re-authorize with specific scopes or scope groups. Use this skill whenever the user asks about their Crab Cloud account ("我的账号信息", "who am I on crabcloud"), wants to see or clean up authorized agents/tokens ("看看我授权了哪些 agent", "revoke that old token", "撤销那个旧令牌"), manages their contacts ("我的联系人", "把 zhangsan 加到联系人", "我的邀请码"), or asks how to connect their coding agent to Crab Cloud — even if they never say "crab" or "Crab Cloud" explicitly. Contacts are managed with `crab contacts …` in this skill; email lives in the crabcloud-mail skill (crab mail …) and the Drive in crabcloud-storage (crab storage …); collab ships in a later phase.
3
+ description: Manage the user's Crab Cloud personal cloud account and agent tokens through the `crab` CLI — check identity/subscription/credits, list and revoke authorized agent tokens, and re-authorize with specific scopes or scope groups. Use this skill whenever the user asks about their Crab Cloud account ("我的账号信息", "who am I on crabcloud"), wants to see or clean up authorized agents/tokens ("看看我授权了哪些 agent", "revoke that old token", "撤销那个旧令牌"), manages their contacts ("我的联系人", "把 zhangsan 加到联系人", "我的邀请码"), or asks how to connect their coding agent to Crab Cloud — even if they never say "crab" or "Crab Cloud" explicitly. Contacts are managed with `crab contacts …` in this skill; email lives in the crabcloud-mail skill (crab mail …), the Drive in crabcloud-storage (crab storage …), and collab in crabcloud-collab (crab collab …).
4
4
  ---
5
5
 
6
6
  # Crab Cloud(平台 / 账号域)
@@ -10,8 +10,8 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
10
10
  (`~/.config/crabcloud/credentials.json`,0600),服务端只存令牌哈希、全程审计。
11
11
  本 skill 覆盖**平台、账号域与联系人**(`crab contacts …`);邮箱见
12
12
  `crabcloud-mail` skill(`crab mail …`),
13
- 云盘见 `crabcloud-storage` skill(`crab storage …`);协作随后续阶段
14
- 提供——用户问到时如实说明,**不要猜命令**。
13
+ 云盘见 `crabcloud-storage` skill(`crab storage …`);协作见
14
+ `crabcloud-collab` skill(`crab collab …`)。
15
15
 
16
16
  ## 第一步:确认 CLI 可用
17
17
 
@@ -45,7 +45,7 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
45
45
 
46
46
  积分命令需 `credits.read` scope(`account` 组含它):退出码 4 报缺该 scope 时,
47
47
  引导 `crab login --scopes account` 重新授权。积分额度一律以**积分数**表述
48
- (100 积分 = $1.00,仅价格类口径用美元);充值/下单是 Web 会话能力,CLI/Agent
48
+ (**1 积分 = $1**,仅价格类口径用美元);充值/下单是 Web 会话能力,CLI/Agent
49
49
  令牌不动账——用户要充值时引导网页账单页(crabcloud.cc/account/billing)。
50
50
  拿不准令牌 id 时先 `crab tokens list` 核对;`revoke` 只接受列表里的 id。
51
51
 
@@ -76,11 +76,11 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
76
76
  不要怂恿用户一次性给全量 scope。`--scopes` 支持组名(`mail` = 读写搜删四项
77
77
  整组)与单个 scope 混用;组目录见 `crab help`,可用组:account / mail /
78
78
  storage(云盘,含素材与附件)/ collab / contacts。
79
- - 如实转述能力边界:协作尚未上线——相关请求如实说明,引导用户
80
- 用网页(crabcloud.cc),不要编造命令或输出;联系人已上线,用 `crab contacts`
79
+ - 如实转述能力边界,不要编造命令或输出:联系人用 `crab contacts`
81
80
  (list / add / rm / invite / invitees;scope contacts.read/write,程序消费加
82
- `--json`)处理;邮箱请求转交 `crabcloud-mail`
83
- skill,云盘请求转交 `crabcloud-storage` skill。
81
+ `--json`)处理;邮箱请求转交 `crabcloud-mail` skill,云盘请求转交
82
+ `crabcloud-storage` skill,协作请求转交 `crabcloud-collab` skill
83
+ (`crab collab …`,已上线)。
84
84
  - 账号安全操作(修改密码、登录设备/会话的查看与吊销)**有意不开放给 agent
85
85
  通道**:改密对令牌持有者是账号接管面,会话属于"人的浏览器会话"。用户让
86
86
  agent 做这些时,如实说明并引导到网页个人中心(crabcloud.cc/account)操作,
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: crabcloud-agent
3
+ description: Check the status of the user's personal cloud Agent in Crab Cloud through the `crab agent` CLI — running sessions and tasks, and the outbound-action approval queue (pending / expired). Use this skill whenever the user asks about their in-cloud agent ("我的 agent 在干嘛", "agent 跑完了吗", "有没有待审批的操作", "看看 agent 审批队列"), wants a quick health check of their Agent workbench, or asks what the agent has been doing — even if they never say "crab" or "Crab Cloud" explicitly. This skill is read-only; approving or rejecting happens in the Agent workbench web UI (agent.crabcloud.cc), not from the CLI.
4
+ ---
5
+
6
+ # 个人云 Agent(查看域)
7
+
8
+ Crab Cloud 的个人云 Agent 是一个用户一个固定 agent:长记忆、权限三档、
9
+ 能读平台数据(邮箱/云盘/协作)、写动作过审批全留痕。本 skill 覆盖**只读
10
+ 查看**(`crab agent …`,需 `agent.read` scope);审批的批准/拒绝在网页
11
+ 工作台完成(流内卡片),CLI 不做决策。
12
+
13
+ ## 第一步:确认可用
14
+
15
+ 先确认 `crab` 命令存在:`crab --help`。若命令缺失,引导安装:
16
+
17
+ ```
18
+ npx @crabcloud/cli init
19
+ ```
20
+
21
+ 已安装但命令缺失时升级:`npx @crabcloud/cli@latest init`。所有
22
+ `crab <cmd>` 都可用 `npx @crabcloud/cli <cmd>` 等价执行。
23
+
24
+ ## 命令
25
+
26
+ ### 状态总览
27
+
28
+ ```
29
+ crab agent status
30
+ ```
31
+
32
+ 输出:会话总数、运行中数量、待审批数量。`--json` 输出机器可读 JSON。
33
+
34
+ ### 会话列表
35
+
36
+ ```
37
+ crab agent sessions
38
+ ```
39
+
40
+ 列出对话与任务(标题 / 状态 / 定时 cron / 最近更新时间)。`--json` 可用。
41
+
42
+ ### 审批队列
43
+
44
+ ```
45
+ crab agent approvals
46
+ ```
47
+
48
+ 列出待处理与已超时的出站审批(工具名 / 参数摘要 / 来源会话)。
49
+ **这里只看,不批**——告诉用户:打开 Agent 工作台(agent.crabcloud.cc)→
50
+ 对应会话的流内审批卡上点「批准执行 / 拒绝」;24 小时未处理的会自动失效,
51
+ 且失效后即使补批也不会执行(agent 需重新发起)。
52
+
53
+ ## 边界
54
+
55
+ - 只读:本 skill 的命令都不会改变 agent 状态;决策一律引导到工作台网页。
56
+ - BYOK 配置(API 端点 / Key)不在 CLI 视野——在「模型」管理页维护。
57
+ - 需要写操作走平台时,外部 agent 用既有域命令:`crab mail …` /
58
+ `crab storage …` / `crab collab …`(各自 skill),与本 skill 互补。
@@ -83,6 +83,10 @@ skill 覆盖协作域的 agent 通道;账号/令牌管理见 `crabcloud` skill
83
83
  补一条更正记录,不要试图「撤回」。
84
84
  - **摘要是可变当前态**:表达「现在进展到哪」,不是流水账;每次推送记录后若
85
85
  摘要已过时,顺手 `crab collab task summary` 刷新,让不看全程的人一眼看懂。
86
+ - **空间级管理走网页端**:改名/归档/调整自分配存储/扩容(任务+20、成员+5)/
87
+ 移除成员是 owner 的网页操作,Agent 令牌一律被服务端拒绝——用户要这些时如实
88
+ 说明并引导网页端,**不要换姿势重试**;建独立空间(`collab space create`)与
89
+ 邀请成员(`collab invite`)例外,Agent 可执行。
86
90
  - **建独立空间是付费动作(5 积分)**:先向用户复述名称与自分配存储(默认
87
91
  500MB,占订阅总池),确认后再执行;`collab space create` 不要当作顺手操作。
88
92
  - **邀请有前置**:目标必须是联系人簿里已关联的平台用户(`crab contacts add`
@@ -90,6 +94,8 @@ skill 覆盖协作域的 agent 通道;账号/令牌管理见 `crabcloud` skill
90
94
  系人,不要换名字硬试。
91
95
  - **附件引用不复制**:`--attach` 的 fileId 是云盘/空间已有文件的引用(≤10 个),
92
96
  只能引用空间成员的云盘文件或本空间直传文件;跨空间文件会被 403 拒绝。
97
+ **只接受明文 / 平台加密(p1)对象,保险箱(u1) 文件引用会被拒**——见
98
+ `crabcloud-storage` skill。
93
99
  - 单任务记录流上限 500 条(状态变更也占位):任务粒度别太粗,做完就 `done`,
94
100
  新工作开新任务。
95
101
  - 读记录流对 Agent 是一等审计事件(thread.read 入活动流)——正常读即可,但
@@ -7,8 +7,8 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
7
7
 
8
8
  用户的 Crab Cloud 账号自带平台邮箱 `username@crabcloud.cc`(用户名即地址本地部分)。
9
9
  本 skill 覆盖邮箱域的 agent 通道;账号/令牌管理见 `crabcloud` skill。云盘见
10
- `crabcloud-storage` skill(`crab storage …`),协作尚未上线——用户问到时如实说明,
11
- **不要猜命令**。
10
+ `crabcloud-storage` skill(`crab storage …`),协作见 `crabcloud-collab` skill
11
+ (`crab collab …`)。
12
12
 
13
13
  ## 第一步:确认可用
14
14
 
@@ -32,6 +32,9 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
32
32
  | 只看未读 | `crab mail list --unread` |
33
33
  | 翻页 | `crab mail list --cursor <nextCursor>`(列表末尾会提示) |
34
34
  | 读某封邮件 | `crab mail read <id>`(自动置已读;正文 + 操作记录) |
35
+ | 标回未读 | `crab mail unread <id>` |
36
+ | 看各文件夹总量/未读、本月收发 | `crab mail folders`(判断「有没有新邮件」先看这个,别翻列表) |
37
+ | 看邮箱操作流(谁读过/谁代发) | `crab mail activity`(最近 50 条,审计源) |
35
38
  | 找邮件 | `crab mail search <关键词>`(主题/发件人/收件人/正文,不含废纸篓) |
36
39
  | 发邮件 | `crab mail send --to a@x.cc --subject "..." --body "..."`(长正文用 `--body-file`) |
37
40
  | 发邮件带附件 | `crab mail send … --attach <云盘id\|文件名>[,…]`(引用云盘已有对象,不复制,≤10 个;先 `crab storage ls` 核对对象,文件名需精确唯一命中) |
@@ -44,7 +47,7 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
44
47
  ## 计费口径(必须如实转述)
45
48
 
46
49
  - **平台内互发免费**:收件人也是 `@crabcloud.cc` 地址时不消耗积分。
47
- - **外部出站 2 积分/封**:发送前向用户复述收件人与计费;退出码 7 = 积分不足,
50
+ - **外部出站 0.02 积分/封($0.02)**:发送前向用户复述收件人与计费;退出码 7 = 积分不足,
48
51
  告知用户不要自动重试。
49
52
  - 发送结果会报告 `消耗积分` 与失败数;部分失败时失败部分的积分自动退回
50
53
  (`发送:partial` 标记)。
@@ -57,7 +60,9 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
57
60
  `id/folder/fromAddr/to/subject/snippet/isUnread/hasAttachments/createdAt`;
58
61
  - `mail read --json` → 完整正文 + `attachments`(id/filename/mimeType/sizeBytes)
59
62
  + `actors`(谁读过/谁代发,审计源);
60
- - `mail send --json` → `{ message, chargedCredits, failedExternal }`。
63
+ - `mail send --json` → `{ message, chargedCredits, failedExternal }`;
64
+ - `mail folders --json` → `{ address, folders: [{ folder, total, unread }], month: { received, sent, outbound } }`;
65
+ - `mail activity --json` → 条目含 `action(read/drafted/sent)/ actorType / actorLabel / subject / messageId / createdAt`。
61
66
 
62
67
  ## 纪律
63
68
 
@@ -71,5 +76,6 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
71
76
  - 附件:`mail read` 输出附件清单(id/文件名/类型/体积);附件内容归云盘管
72
77
  ——下载用 `crab storage download <id>`(见 `crabcloud-storage` skill,跨来
73
78
  源通用),发送引用云盘对象用 `mail send --attach`(对象须未被其他邮件绑
74
- 定;若报错说明已被引用,改走正文链接)。邮件详情附件显示「已在云盘删除」
75
- 表示对象已被软删,如实转述。
79
+ 定;若报错说明已被引用,改走正文链接)。**附件只接受明文 / 平台加密(p1)
80
+ 对象,保险箱(u1) 文件不能作附件**——CLI 会明确拒绝,需要分享时改走正文
81
+ 链接。邮件详情附件显示「已在云盘删除」表示对象已被软删,如实转述。
@@ -1,15 +1,18 @@
1
1
  ---
2
2
  name: crabcloud-storage
3
- description: Manage the user's Crab Cloud Drive (云盘 — unified storage for drive uploads, media assets and mail attachments on one quota, with end-to-end encryption) through the `crab` CLI — list and filter objects, upload and download files (including decrypting share links), rename, tag, categorize, publish/unpublish public links, create expiring share links with passcodes, manage the locally remembered encryption key, trash/restore/purge, and check the storage quota. Use this skill when the user asks to upload, find, organize, share or clean up files on Crab Cloud ("上传这个文件", "云盘里有什么", "把这个文件公开分享", "看看存储用了多少", "crab storage"), or wants files moved in or out of their drive — even if they never say "crab" or "Crab Cloud" explicitly.
3
+ description: Manage the user's Crab Cloud storage (云盘 — one base file service carrying drive uploads, media assets, mail attachments, collab files and agent files on one quota; vault-grade end-to-end encryption is optional) through the `crab` CLI — list and filter objects, upload and download files (including decrypting share links), rename, tag, categorize, publish/unpublish public links, create expiring share links with passcodes, manage the locally remembered encryption key, trash/restore/purge, and check the storage quota. Use this skill when the user asks to upload, find, organize, share or clean up files on Crab Cloud ("上传这个文件", "云盘里有什么", "把这个文件公开分享", "看看存储用了多少", "crab storage"), or wants files moved in or out of their drive — even if they never say "crab" or "Crab Cloud" explicitly.
4
4
  ---
5
5
 
6
6
  # Crab Cloud 云盘(crab storage)
7
7
 
8
- 云盘是用户的统一存储:云盘上传 / 素材库 / 邮件附件三来源共用一本 2 GB 容量账;
9
- 属性优先、位置无关——分类(至多一个)+ 标签(AND 检索)代替文件夹。files 域
10
- 上传默认端到端加密(服务器只有包裹态密钥,明文密钥只在本机)。本 skill 覆盖
11
- 云盘域的 agent 通道;账号/令牌管理见 `crabcloud` skill,邮箱见 `crabcloud-mail`
12
- skill。
8
+ 云盘是用户的统一存储基础服务:云盘上传 / 素材库 / 邮件附件 / 协作文件 /
9
+ Agent 产物五来源共用一本容量账(按档位,Free 1GB / Plus 10GB);属性优先、
10
+ 位置无关——分类(至多一个)+ 标签(AND 检索)代替文件夹。**上传缺省明文**
11
+ (2026-10 基础服务化:其他应用可直接引用);提供口令时才端到端加密(保险箱
12
+ 文件,服务器只有包裹态密钥,明文密钥只在本机)。加密三态以 `wrappedDataKey`
13
+ 前缀区分:无前缀 = 明文、`p1.` = 平台加密(如邮件附件,服务端可透明解密)、
14
+ `u1.` = 保险箱(用户持钥)。本 skill 覆盖存储域的 agent 通道;账号/令牌管理见
15
+ `crabcloud` skill,邮箱见 `crabcloud-mail` skill。
13
16
 
14
17
  ## 第一步:确认可用
15
18
 
@@ -24,7 +27,7 @@ skill。
24
27
  `storage.write`,移回收站/恢复/彻底删除 `storage.delete`。退出码 4 = scope
25
28
  不足:如实转述缺哪个,引导 `crab login --scopes storage`(组名 = 三项整组)
26
29
  重新授权(你只发起,不代批)。
27
- - 退出码 7 = `storage.quotaExceeded`(统一容量账满,2 GB):告知用户,可建议
30
+ - 退出码 7 = `storage.quotaExceeded`(统一容量账满):告知用户,可建议
28
31
  清理回收站或大文件(`storage quota` 看分项),不要自动重试。
29
32
 
30
33
  ## 意图 → 命令映射
@@ -32,9 +35,9 @@ skill。
32
35
  | 用户意图 | 命令 |
33
36
  |---|---|
34
37
  | 云盘里有什么 | `crab storage ls`(按时间倒序,默认 30 条;`--limit` / `--offset` 翻页) |
35
- | 找文件 | `crab storage ls --q 关键字`(文件名匹配;可叠 `--source files\|media\|mail`、`--type doc\|image\|video\|archive\|code`、`--category 分类id\|none`、`--tag a,b`、`--shared`、`--recent`、`--message-id <id>`) |
38
+ | 找文件 | `crab storage ls --q 关键字`(文件名匹配;可叠 `--source files\|media\|mail\|collab\|agent`、`--type doc\|image\|video\|archive\|code`、`--category 分类id\|none`、`--tag a,b`、`--shared`、`--recent`、`--encrypted only\|plain`、`--message-id <id>`;`--encrypted only` = 仅保险箱、`plain` = 排除保险箱) |
36
39
  | 看回收站 | `crab storage ls --trash` |
37
- | 上传文件 | `crab storage upload <路径...>`(多文件串行;直传优先,自动回退中转;默认端到端加密——口令三来源见下节,首次使用自动初始化并打印一次性恢复码) |
40
+ | 上传文件 | `crab storage upload <路径...>`(多文件串行;直传优先,自动回退中转;**缺省明文直传**,其他应用可直接引用;`--passphrase` 或 `CRAB_PASSPHRASE` 提供时端到端加密为保险箱文件——口令三来源见下节,首次使用自动初始化并打印一次性恢复码) |
38
41
  | 下载文件 | `crab storage download <id\|文件名\|分享链接> [-o 输出路径] [--passcode 提取码]`(默认按原文件名存当前目录;**凭完整分享链接可免登录下载并本地解密**——`#` 后是密钥片段,这是 agent→agent 的主通道;带提取码的分享加 `--passcode`) |
39
42
  | 改名 | `crab storage rename <id\|文件名> <新名称>` |
40
43
  | 加/删标签 | `crab storage tag <id\|文件名> --add a,b [--remove c,d]` |
@@ -46,7 +49,8 @@ skill。
46
49
  | 可控分享(有效期/提取码/撤销) | `crab storage share <id\|文件名> [--expires 7\|30\|90\|forever] [--passcode 提取码]`(打印完整分享链接;加密对象链接自带 `#` 密钥片段);列表 `crab storage shares <id\|文件名>`、撤销 `crab storage unshare <shareId>` |
47
50
  | 云盘钥匙管理 | `crab storage key`(查状态)/ `--remember`(解锁一次并记住,之后免口令)/ `--forget`(清除)——见下节 |
48
51
  | 删除 | `crab storage rm <id\|文件名>`(进回收站,7 天后自动清理)→ `restore` 恢复 / `purge` 彻底删除 |
49
- | 看水位 | `crab storage quota`(三来源分项 + 回收站占用 + 对象计数) |
52
+ | 看水位 | `crab storage quota`(五来源分项 + 保险箱计数 + 回收站占用 + 对象计数) |
53
+ | 看云盘操作流 | `crab storage activity`(读取/上传/删除/建分享,最近 50 条) |
50
54
 
51
55
  `<id|文件名>`:文件名全库精确唯一命中时自动解析为 id(回收站内同样可解析,
52
56
  restore/purge 直接用文件名即可);命中多个或查不到时按字面当 id 用——拿不准
@@ -58,16 +62,20 @@ restore/purge 直接用文件名即可);命中多个或查不到时按字面
58
62
  程序化消费**始终加 `--json`**:
59
63
 
60
64
  - `storage ls --json` → `{ objects: [...], nextOffset, total }`,条目含
61
- `id / source / filename / mimeType / sizeBytes / categoryId / tags / isPublic / createdAt`;
62
- - `storage quota --json` → `{ usedBytes, quotaBytes, bySource: { files, media, mail }, trashBytes, counts }`;
65
+ `id / source / filename / mimeType / sizeBytes / categoryId / tags / isEncrypted / wrappedDataKey / isPublic / createdAt`
66
+ (`source` = `files|media|mail|collab|agent`;u1 判定 = `isEncrypted && wrappedDataKey` 以 `u1.` 开头);
67
+ - `storage quota --json` → `{ usedBytes, quotaBytes, bySource: { files, media, mail, collab, agent }, trashBytes, counts: { all, files, media, mail, collab, agent, shared, vault, trash } }`
68
+ (素材库条数 = `counts.all - counts.vault`);
69
+ - `storage activity --json` → 条目含 `action(object.read/object.uploaded/object.deleted/share.created)/ actorType / actorLabel / objectId / objectName / createdAt`;
63
70
  - `upload` / `categories` / `tags` / `shares` 的 `--json` 同构(单对象 / 分类数组 /
64
71
  标签数组 / 分享数组)。
65
72
 
66
- ## 云盘口令与本机钥匙
73
+ ## 保险箱口令与本机钥匙
67
74
 
68
- files 域是端到端加密:服务器只有口令包裹态的主密钥,**明文密钥只存在于用户
69
- 本机**——CLI 不可能也不允许向服务器索取。涉及加密对象的命令(upload /
70
- download / share 加密文件)需要云盘口令,来源优先级:
75
+ 保险箱(u1)是端到端加密:服务器只有口令包裹态的主密钥,**明文密钥只存在于
76
+ 用户本机**——CLI 不可能也不允许向服务器索取。**只有涉及保险箱文件的命令才
77
+ 需要口令**(带口令 upload / download 或 share u1 对象;明文与 p1 平台加密对象
78
+ 全程免口令)。口令来源优先级:
71
79
 
72
80
  1. `--passphrase <口令>`(flag)
73
81
  2. 环境变量 `CRAB_PASSPHRASE`(**Agent/脚本推荐**——非交互终端不会弹提问,
@@ -94,6 +102,9 @@ download / share 加密文件)需要云盘口令,来源优先级:
94
102
  等待即可;凭链接下载时提取码用 `--passcode` 正确携带。
95
103
  - **purge 是彻底删除**,不可恢复;用户没明说「彻底删除」时一律用 `rm`
96
104
  (回收站保留 7 天可找回)。
105
+ - **保险箱(u1)不能被其他应用引用**:邮件附件(`mail send --attach`)与协作
106
+ 记录引用都只接受明文 / p1 平台加密对象,u1 对象会被明确拒绝——需要跨应用
107
+ 使用时用分享链接(u1 链接自带密钥片段),或等互转功能;不要尝试绕过。
97
108
  - 邮件附件与用户上传同账:水位告急时用 `quota` 分项定位来源再建议清理。
98
109
  - 公开直链的呈现方式由服务端策略决定(图片/视频/音频/PDF/纯文本内联,SVG 与
99
110
  其他类型强制下载),CLI 侧没有、也不需要有开关。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crabcloud/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Crab Cloud CLI (crab) — agent-first entry to your personal cloud, with skills bundled for Claude Code / Codex / ZCode.",
6
6
  "bin": {