@crabcloud/cli 0.1.9 → 0.2.0

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(),
@@ -5886,6 +6063,13 @@ var adminPlanTierUpdateInput = object({
5886
6063
  quotaJson: record(string2(), unknown()).optional(),
5887
6064
  status: _enum(adminPlanTierStatuses).optional()
5888
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
+ });
5889
6073
  var giftFulfillments = ["stock", "webhook"];
5890
6074
  var giftMerchantStatuses = ["active", "disabled"];
5891
6075
  var giftProductStatuses = ["draft", "live", "delisted"];
@@ -5990,6 +6174,49 @@ var contactQueryInput = object({
5990
6174
  limit: number2().int().min(1).max(50).default(20),
5991
6175
  offset: number2().int().min(0).default(0)
5992
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
+ });
5993
6220
 
5994
6221
  // ../../packages/cloudkit/src/crypto.ts
5995
6222
  var enc = new TextEncoder;
@@ -6276,6 +6503,9 @@ function exitCodeForApiError(body) {
6276
6503
  case "insufficient_credits":
6277
6504
  case "storage.quotaExceeded":
6278
6505
  return EXIT_QUOTA;
6506
+ case "collab_space_not_bound":
6507
+ case "collab_space_mismatch":
6508
+ return EXIT_SCOPE;
6279
6509
  default:
6280
6510
  return 1;
6281
6511
  }
@@ -6309,6 +6539,9 @@ function requireCredentials() {
6309
6539
  function formatTime(ms) {
6310
6540
  return new Date(ms).toLocaleString("sv", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
6311
6541
  }
6542
+ function fmtCredits(cents) {
6543
+ return cents.toLocaleString("en-US");
6544
+ }
6312
6545
  var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6313
6546
  发布物经 npm 分发:npx @crabcloud/cli <command>(全局安装后短名 crab 等价)
6314
6547
 
@@ -6320,6 +6553,9 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6320
6553
  login 浏览器授权登录(设备授权流),保存 Agent Token
6321
6554
  --scopes <scope|组名,...> 自定义 scope,支持组名整组授权
6322
6555
  (默认 ${DEFAULT_SCOPES.join(",")})
6556
+ --space <default|space:<名称>>
6557
+ 含协作 scope 时必填:令牌绑定该协作
6558
+ 空间,collab 命令只能作用于它
6323
6559
  --no-open 只打印链接,不自动打开浏览器
6324
6560
  --api <url> 指定 API 地址
6325
6561
 
@@ -6371,6 +6607,34 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6371
6607
  storage categories [--json] 分类列表
6372
6608
  storage mkcat <名称> 新建分类
6373
6609
  storage tags [--json] 标签汇总
6610
+ collab spaces [--json] 列出我所在的协作空间(任务/成员计数与限额)
6611
+ collab space create <名称> [--allocation-mb N] [--json]
6612
+ 创建独立协作空间(消耗 5 积分;默认自分配 500MB
6613
+ 存储占订阅总池,可随时调)
6614
+ collab tasks [--space <arg>] [--status open|done] [--q 关键字]
6615
+ [--limit N] [--cursor <cursor>] [--json]
6616
+ 列出任务(含摘要/记录数;--q 搜标题与记录正文)
6617
+ collab task <id> [--json] 任务详情
6618
+ collab task create --space <arg> --title "..." [--body "..." | --body-file <path>]
6619
+ [--attach <fileId,...>] [--link <url,...>] [--json]
6620
+ 新建任务(首条记录即 body 入流)
6621
+ collab task records <id> [--limit N] [--cursor <cursor>] [--json]
6622
+ 读任务记录流(倒序,Markdown 源文 + 附件/链接)
6623
+ collab task done|reopen <id> 完成 / 重开(入流一条状态变更,占记录位)
6624
+ collab task summary <id> (--text "..." | --text-file <path> | --clear) [--json]
6625
+ 更新任务摘要(可变「当前态」概览,不入记录流;
6626
+ 上限 2000 字符,--clear 清空)
6627
+ collab members [--space <arg>] [--json]
6628
+ 协作空间成员列表
6629
+ collab invite <username> [--space <arg>] [--json]
6630
+ 邀请联系人中已关联的平台用户进空间
6631
+ collab activity [--json] 协作域活动事件(Agent 推送/读取、任务创建、摘要更新)
6632
+ post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]
6633
+ 推送任务记录(糖命令:等价 collab task records 的写入)
6634
+ credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
6635
+ credits history [--limit N] [--offset N] [--json]
6636
+ 积分流水(变动 ±:消耗为负;需 credits.read)
6637
+ credits pricing [--json] 动作计价标准(公开:动作单价/档位/充值包)
6374
6638
  skills install|list|uninstall [--agent claude,zcode,codex,agents]
6375
6639
  skills 管理(claude/zcode/agents 复制目录,codex 写 AGENTS.md 段)
6376
6640
  logout 撤销当前 Agent Token 并清除本地凭证
@@ -6384,7 +6648,11 @@ ${scopeGroups.map((group) => ` ${group.id.padEnd(9)} ${group.scopes.join(", ")}
6384
6648
  CRAB_API API 地址(默认 ${DEFAULT_API})
6385
6649
  CRAB_CREDENTIALS 凭证文件路径(默认 ~/.config/crabcloud/credentials.json)
6386
6650
 
6387
- 退出码: 0 成功 · 2 用法错误 · 3 认证失效 · 4 scope 不足 · 5 资源不存在 · 7 配额/积分不足
6651
+ collab 命令的 <arg>(--space 值): default(默认空间)| space:<名称>(按名唯一命中)| 空间 id;
6652
+ 缺省时你只在一个空间则自动采用,多个则列出候选要求显式指定。Agent 令牌绑定空间后,
6653
+ 越界访问一律 403(collab_space_mismatch)。
6654
+
6655
+ 退出码: 0 成功 · 2 用法错误 · 3 认证失效 · 4 scope 不足(含令牌未绑/绑错协作空间)· 5 资源不存在 · 7 配额/积分不足
6388
6656
  `;
6389
6657
  async function login() {
6390
6658
  const apiBase = process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
@@ -6400,10 +6668,16 @@ async function login() {
6400
6668
  console.error(`--scopes 展开后为空(输入:${rawScopes.join(", ")})`);
6401
6669
  return EXIT_USAGE;
6402
6670
  }
6671
+ const spaceArg = readFlag("--space");
6672
+ if (requested.some((s) => s === "collab.read" || s === "collab.write") && !spaceArg) {
6673
+ console.error("含协作 scope(collab)时必须指定 --space <default|space:<名称>>——令牌将绑定该协作空间,越界访问一律拒绝。");
6674
+ console.error("示例:crab login --scopes collab --space default");
6675
+ return EXIT_USAGE;
6676
+ }
6403
6677
  const openBrowser = !hasFlag("--no-open");
6404
6678
  const start = await api2(apiBase, "/v1/auth/device/start", {
6405
6679
  method: "POST",
6406
- body: JSON.stringify({ name: `${hostname()} · crab cli`, scopes: requested })
6680
+ body: JSON.stringify({ name: `${hostname()} · crab cli`, scopes: requested, ...spaceArg ? { space: spaceArg } : {} })
6407
6681
  });
6408
6682
  if (start.status !== 200) {
6409
6683
  console.error(`发起授权失败(${start.status}):`, errorMessage(start.body, JSON.stringify(start.body)));
@@ -6445,6 +6719,8 @@ async function login() {
6445
6719
  saveCredentials({ apiBase, token: poll.body.data.token, scopes: poll.body.data.scopes });
6446
6720
  console.log(`✓ 已登录,Agent Token 已保存到 ${CONFIG_PATH}`);
6447
6721
  console.log(` scopes: ${poll.body.data.scopes.join(", ")}`);
6722
+ if (spaceArg)
6723
+ console.log(` 协作空间绑定: ${spaceArg}(collab 命令只能作用于该空间)`);
6448
6724
  return 0;
6449
6725
  }
6450
6726
  }
@@ -6473,11 +6749,105 @@ async function whoami() {
6473
6749
  if (subscription)
6474
6750
  console.log(` 订阅: ${subscription.tier_name}`);
6475
6751
  if (credits !== undefined)
6476
- console.log(` 积分余额: $${(credits / 100).toFixed(2)} (${credits} credit_cents)`);
6752
+ console.log(` 积分余额: ${fmtCredits(credits)} 积分`);
6477
6753
  if (apps?.length)
6478
6754
  console.log(` 已启用应用: ${apps.map((a) => a.title).join("、")}`);
6479
6755
  return 0;
6480
6756
  }
6757
+ var CREDIT_REASON_LABELS = {
6758
+ subscription_grant: "月送发放",
6759
+ topup: "充值入账",
6760
+ consume: "动作消耗",
6761
+ refund: "退款",
6762
+ adjustment: "人工调整"
6763
+ };
6764
+ var CREDIT_ACTION_LABELS = {
6765
+ "mail.send_outbound": "外部出站邮件",
6766
+ "collab.space_create": "创建独立协作空间",
6767
+ "collab.boost_tasks": "协作任务扩容(+20)",
6768
+ "collab.boost_members": "协作成员扩容(+5)"
6769
+ };
6770
+ function creditMovement(entry) {
6771
+ if (entry.grantedDelta === null && entry.topupDelta === null)
6772
+ return entry.delta;
6773
+ return (entry.grantedDelta ?? 0) + (entry.topupDelta ?? 0);
6774
+ }
6775
+ async function fetchCredits(credentials, limit, offset) {
6776
+ const res = await api2(credentials.apiBase, `/v1/me/credits?limit=${limit}&offset=${offset}`, { headers: authHeaders(credentials) });
6777
+ if (res.status !== 200) {
6778
+ console.error(errorMessage(res.body, "获取积分数据失败。"));
6779
+ if (res.body?.error?.code === "scope_denied") {
6780
+ console.error("提示:积分命令需要 credits.read scope——运行 crab login --scopes account 重新授权。");
6781
+ }
6782
+ return exitCodeForApiError(res.body);
6783
+ }
6784
+ return res.body.data;
6785
+ }
6786
+ async function creditsBalance() {
6787
+ const credentials = requireCredentials();
6788
+ if (typeof credentials === "number")
6789
+ return credentials;
6790
+ const data = await fetchCredits(credentials, 1, 0);
6791
+ if (typeof data === "number")
6792
+ return data;
6793
+ if (hasFlag("--json")) {
6794
+ console.log(JSON.stringify(data.balance, null, 2));
6795
+ return 0;
6796
+ }
6797
+ console.log(`积分余额:${fmtCredits(data.balance.balance)}`);
6798
+ console.log(` 月送 ${fmtCredits(data.balance.granted)}(每周期刷新清零) · 充值 ${fmtCredits(data.balance.topup)}(长期有效)`);
6799
+ return 0;
6800
+ }
6801
+ async function creditsHistory() {
6802
+ const credentials = requireCredentials();
6803
+ if (typeof credentials === "number")
6804
+ return credentials;
6805
+ const limit = Math.min(Math.max(Number.parseInt(readFlag("--limit") ?? "", 10) || 20, 1), 100);
6806
+ const offset = Math.max(Number.parseInt(readFlag("--offset") ?? "", 10) || 0, 0);
6807
+ const data = await fetchCredits(credentials, limit, offset);
6808
+ if (typeof data === "number")
6809
+ return data;
6810
+ if (hasFlag("--json")) {
6811
+ console.log(JSON.stringify(data, null, 2));
6812
+ return 0;
6813
+ }
6814
+ if (data.ledger.length === 0) {
6815
+ console.log("还没有积分流水。");
6816
+ return 0;
6817
+ }
6818
+ for (const entry of data.ledger) {
6819
+ const movement = creditMovement(entry);
6820
+ const signed = movement > 0 ? `+${fmtCredits(movement)}` : fmtCredits(movement);
6821
+ console.log(`${formatTime(entry.createdAt)} ${CREDIT_REASON_LABELS[entry.reason] ?? entry.reason} ${signed}`);
6822
+ }
6823
+ console.log(`
6824
+ 共 ${fmtCredits(data.total)} 条(本页 ${data.ledger.length} 条,--offset 翻页)`);
6825
+ return 0;
6826
+ }
6827
+ async function creditsPricing() {
6828
+ const credentials = loadCredentials();
6829
+ const apiBase = credentials ? credentials.apiBase : process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
6830
+ const res = await api2(apiBase, "/v1/pricing/standard");
6831
+ if (res.status !== 200) {
6832
+ console.error(errorMessage(res.body, "获取动作计价失败。"));
6833
+ return exitCodeForApiError(res.body);
6834
+ }
6835
+ const actions = res.body.data?.actions ?? [];
6836
+ if (hasFlag("--json")) {
6837
+ console.log(JSON.stringify(res.body.data, null, 2));
6838
+ return 0;
6839
+ }
6840
+ if (actions.length === 0) {
6841
+ console.log("暂无动作计价项。");
6842
+ return 0;
6843
+ }
6844
+ console.log("动作计价(credit_cents,100 = $1.00):");
6845
+ for (const item of actions) {
6846
+ const label = CREDIT_ACTION_LABELS[item.action] ?? item.action;
6847
+ console.log(` ${item.action.padEnd(24)} ${label} ${fmtCredits(item.creditCents)} 积分`);
6848
+ }
6849
+ return 0;
6850
+ }
6481
6851
  async function tokensList() {
6482
6852
  const credentials = requireCredentials();
6483
6853
  if (typeof credentials === "number")
@@ -6497,7 +6867,7 @@ async function tokensList() {
6497
6867
  return 0;
6498
6868
  }
6499
6869
  for (const token of rows) {
6500
- console.log(`${token.id} ${token.name} [${token.scopes.join(", ")}] ${token.revoked ? "已撤销" : "生效中"}`);
6870
+ console.log(`${token.id} ${token.name} [${token.scopes.join(", ")}] ${token.revoked ? "已撤销" : "生效中"}${token.collabSpace ? ` ⟨空间 ${token.collabSpace}⟩` : ""}`);
6501
6871
  console.log(` 创建 ${formatTime(token.createdAt)} · 最近使用 ${token.lastUsedAt ? formatTime(token.lastUsedAt) : "—"}${token.expiresAt ? ` · 过期 ${formatTime(token.expiresAt)}` : ""}`);
6502
6872
  }
6503
6873
  return 0;
@@ -6752,7 +7122,7 @@ async function mailSend() {
6752
7122
  if (attachmentIds.length > 0)
6753
7123
  console.log(` 附件 ${attachmentIds.length} 个(云盘引用,不复制)`);
6754
7124
  if (result.chargedCredits > 0)
6755
- console.log(` 消耗积分:$${(result.chargedCredits / 100).toFixed(2)}(${result.chargedCredits} credit_cents)`);
7125
+ console.log(` 消耗积分:${fmtCredits(result.chargedCredits)}`);
6756
7126
  if (result.failedExternal > 0)
6757
7127
  console.log(` ⚠ ${result.failedExternal} 个外部收件人发送失败,对应积分已退回`);
6758
7128
  return 0;
@@ -7574,6 +7944,487 @@ async function storageTags() {
7574
7944
  console.log(`${row.tag} (${row.count})`);
7575
7945
  return 0;
7576
7946
  }
7947
+ var COLLAB_TASK_STATUSES = ["open", "done"];
7948
+ function collabSpaceToken(space) {
7949
+ return space.kind === "default" ? "default" : `space:${space.name}`;
7950
+ }
7951
+ function printCollabTaskRow(task) {
7952
+ const flags = [task.status === "done" ? "已完成" : null, task.summary ? "有摘要" : null].filter(Boolean);
7953
+ console.log(`${task.id} ${task.title} 记录 ${task.recordCount} ${formatTime(task.latestRecordAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
7954
+ }
7955
+ async function resolveCollabSpace(credentials, arg) {
7956
+ const res = await api2(credentials.apiBase, "/v1/collab/spaces", { headers: authHeaders(credentials) });
7957
+ if (res.status !== 200) {
7958
+ console.error(errorMessage(res.body, "获取协作空间失败。"));
7959
+ if (res.body?.error?.code === "scope_denied") {
7960
+ console.error("提示:协作命令需要 collab scope——运行 crab login --scopes collab --space default 重新授权。");
7961
+ }
7962
+ return exitCodeForApiError(res.body);
7963
+ }
7964
+ const spaces = res.body.data.spaces;
7965
+ if (spaces.length === 0) {
7966
+ console.error("你还没有可访问的协作空间。");
7967
+ return EXIT_NOT_FOUND;
7968
+ }
7969
+ if (arg === undefined || arg === "") {
7970
+ if (spaces.length === 1)
7971
+ return spaces[0];
7972
+ console.error("你所在的协作空间不止一个,请用 --space 指定(default / space:<名称> / 空间 id):");
7973
+ for (const s of spaces)
7974
+ console.error(` ${collabSpaceToken(s)} (任务 ${s.taskCount})`);
7975
+ return EXIT_USAGE;
7976
+ }
7977
+ if (arg === "default") {
7978
+ const hit = spaces.find((s) => s.kind === "default");
7979
+ if (hit)
7980
+ return hit;
7981
+ console.error("没有找到你的默认协作空间。");
7982
+ return EXIT_NOT_FOUND;
7983
+ }
7984
+ if (arg.startsWith("space:")) {
7985
+ const name = arg.slice("space:".length).trim();
7986
+ if (!name) {
7987
+ console.error("space: 后面要跟空间名称(crab collab spaces 查看)。");
7988
+ return EXIT_USAGE;
7989
+ }
7990
+ const hit = spaces.filter((s) => s.name === name && s.status === "active");
7991
+ if (hit.length === 1)
7992
+ return hit[0];
7993
+ console.error(hit.length === 0 ? `没有名为「${name}」的 active 协作空间。` : `「${name}」命中多个空间,请用空间 id 指定。`);
7994
+ return EXIT_NOT_FOUND;
7995
+ }
7996
+ const byId = spaces.find((s) => s.id === arg);
7997
+ if (byId)
7998
+ return byId;
7999
+ console.error(`空间标识无效:${arg}
8000
+ 应为 default、space:<名称> 或空间 id(crab collab spaces 查看)。`);
8001
+ return EXIT_USAGE;
8002
+ }
8003
+ async function collabSpacesList() {
8004
+ const credentials = requireCredentials();
8005
+ if (typeof credentials === "number")
8006
+ return credentials;
8007
+ const res = await api2(credentials.apiBase, "/v1/collab/spaces", { headers: authHeaders(credentials) });
8008
+ if (res.status !== 200) {
8009
+ console.error(errorMessage(res.body, "获取协作空间失败。"));
8010
+ return exitCodeForApiError(res.body);
8011
+ }
8012
+ const data = res.body.data;
8013
+ if (hasFlag("--json")) {
8014
+ console.log(JSON.stringify(data, null, 2));
8015
+ return 0;
8016
+ }
8017
+ if (data.spaces.length === 0) {
8018
+ console.log("你还没有可访问的协作空间。");
8019
+ return 0;
8020
+ }
8021
+ for (const s of data.spaces) {
8022
+ const storage = s.kind === "independent" ? ` · 存储 ${formatBytesCli(s.storageUsedBytes)}/${formatBytesCli(s.storageAllocatedBytes)}` : "";
8023
+ console.log(`${collabSpaceToken(s)} ${s.status === "archived" ? "[已归档·只读] " : ""}任务 ${s.taskCount}/${s.limits.taskLimit} · 受邀成员 ${s.memberCount}/${s.limits.invitedMemberLimit}${storage}`);
8024
+ console.log(` id ${s.id}`);
8025
+ }
8026
+ console.log(`独立空间名额:${data.independentUsed}/${data.independentLimit}(创建消耗 5 积分:crab collab space create <名称>)`);
8027
+ return 0;
8028
+ }
8029
+ async function collabSpaceCreate(name) {
8030
+ if (!name) {
8031
+ console.error("用法:crab collab space create <名称> [--allocation-mb <兆字节>] [--json]");
8032
+ return EXIT_USAGE;
8033
+ }
8034
+ const credentials = requireCredentials();
8035
+ if (typeof credentials === "number")
8036
+ return credentials;
8037
+ const allocationMb = Number.parseInt(readFlag("--allocation-mb") ?? "", 10);
8038
+ const body = { name };
8039
+ if (Number.isFinite(allocationMb))
8040
+ body.allocationBytes = allocationMb * 1024 * 1024;
8041
+ const res = await api2(credentials.apiBase, "/v1/collab/spaces", {
8042
+ method: "POST",
8043
+ headers: authHeaders(credentials),
8044
+ body: JSON.stringify(body)
8045
+ });
8046
+ if (res.status !== 200 && res.status !== 201) {
8047
+ console.error(errorMessage(res.body, "创建失败。"));
8048
+ return exitCodeForApiError(res.body);
8049
+ }
8050
+ const data = res.body.data;
8051
+ if (hasFlag("--json")) {
8052
+ console.log(JSON.stringify(data, null, 2));
8053
+ return 0;
8054
+ }
8055
+ console.log(`✓ 独立协作空间已创建(id ${data.spaceId})`);
8056
+ console.log(` 消耗积分:${fmtCredits(data.chargedCredits)} · --space space:${name} 即可指向它`);
8057
+ return 0;
8058
+ }
8059
+ async function collabTasksList() {
8060
+ const credentials = requireCredentials();
8061
+ if (typeof credentials === "number")
8062
+ return credentials;
8063
+ const status = readFlag("--status");
8064
+ if (status && !COLLAB_TASK_STATUSES.includes(status)) {
8065
+ console.error(`未知状态:${status}
8066
+ 可用:${COLLAB_TASK_STATUSES.join(", ")}`);
8067
+ return EXIT_USAGE;
8068
+ }
8069
+ const space = await resolveCollabSpace(credentials, readFlag("--space"));
8070
+ if (typeof space === "number")
8071
+ return space;
8072
+ const params = new URLSearchParams;
8073
+ if (status)
8074
+ params.set("status", status);
8075
+ const q = readFlag("--q");
8076
+ if (q)
8077
+ params.set("q", q);
8078
+ const limit = readFlag("--limit");
8079
+ if (limit)
8080
+ params.set("limit", limit);
8081
+ const cursor = readFlag("--cursor");
8082
+ if (cursor)
8083
+ params.set("cursor", cursor);
8084
+ const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/tasks?${params}`, {
8085
+ headers: authHeaders(credentials)
8086
+ });
8087
+ if (res.status !== 200) {
8088
+ console.error(errorMessage(res.body, "获取任务列表失败。"));
8089
+ return exitCodeForApiError(res.body);
8090
+ }
8091
+ const page = res.body.data;
8092
+ if (hasFlag("--json")) {
8093
+ console.log(JSON.stringify({ ...page, space: { id: space.id, kind: space.kind, name: space.name } }, null, 2));
8094
+ return 0;
8095
+ }
8096
+ console.log(`空间:${collabSpaceToken(space)}${space.status === "archived" ? "(已归档·只读)" : ""}`);
8097
+ if (page.tasks.length === 0) {
8098
+ console.log('没有匹配的任务(crab collab task create --title "…" 新建)。');
8099
+ return 0;
8100
+ }
8101
+ for (const task of page.tasks)
8102
+ printCollabTaskRow(task);
8103
+ if (page.nextCursor)
8104
+ console.log(`(还有更多:--cursor ${page.nextCursor})`);
8105
+ return 0;
8106
+ }
8107
+ async function collabTaskDetail(id) {
8108
+ if (!id) {
8109
+ console.error("用法:crab collab task <id>(id 见 crab collab tasks)");
8110
+ return EXIT_USAGE;
8111
+ }
8112
+ const credentials = requireCredentials();
8113
+ if (typeof credentials === "number")
8114
+ return credentials;
8115
+ const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}`, { headers: authHeaders(credentials) });
8116
+ if (res.status !== 200) {
8117
+ console.error(errorMessage(res.body, "获取任务失败。"));
8118
+ return exitCodeForApiError(res.body);
8119
+ }
8120
+ const task = res.body.data.task;
8121
+ if (hasFlag("--json")) {
8122
+ console.log(JSON.stringify(task, null, 2));
8123
+ return 0;
8124
+ }
8125
+ console.log(`${task.title} [${task.status === "done" ? "已完成" : "进行中"}]`);
8126
+ console.log(` id: ${task.id}`);
8127
+ console.log(` 记录 ${task.recordCount} 条 · 最新 ${formatTime(task.latestRecordAt)} · 创建 ${formatTime(task.createdAt)}`);
8128
+ if (task.summary)
8129
+ console.log(` 摘要: ${task.summary}`);
8130
+ console.log(" 记录流:crab collab task records " + task.id);
8131
+ return 0;
8132
+ }
8133
+ async function collabTaskCreate() {
8134
+ const title = readFlag("--title");
8135
+ const spaceArg = readFlag("--space");
8136
+ const bodyFlag = readFlag("--body");
8137
+ const bodyFile = readFlag("--body-file");
8138
+ if (!title) {
8139
+ console.error('用法:crab collab task create --space <arg> --title "..." [--body "..." | --body-file <path>] [--attach <fileId,...>] [--link <url,...>] [--json]');
8140
+ return EXIT_USAGE;
8141
+ }
8142
+ if (bodyFlag && bodyFile) {
8143
+ console.error("--body 与 --body-file 二选一。");
8144
+ return EXIT_USAGE;
8145
+ }
8146
+ let body = bodyFlag ?? "";
8147
+ if (bodyFile) {
8148
+ try {
8149
+ body = readFileSync2(bodyFile, "utf8");
8150
+ } catch {
8151
+ console.error(`读取正文文件失败:${bodyFile}`);
8152
+ return EXIT_USAGE;
8153
+ }
8154
+ }
8155
+ const credentials = requireCredentials();
8156
+ if (typeof credentials === "number")
8157
+ return credentials;
8158
+ const space = await resolveCollabSpace(credentials, spaceArg);
8159
+ if (typeof space === "number")
8160
+ return space;
8161
+ const fileIds = (readFlag("--attach") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
8162
+ const links = (readFlag("--link") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
8163
+ const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/tasks`, {
8164
+ method: "POST",
8165
+ headers: authHeaders(credentials),
8166
+ body: JSON.stringify({ title, body, ...fileIds.length > 0 ? { fileIds } : {}, ...links.length > 0 ? { links } : {} })
8167
+ });
8168
+ if (res.status !== 200 && res.status !== 201) {
8169
+ console.error(errorMessage(res.body, "创建任务失败。"));
8170
+ return exitCodeForApiError(res.body);
8171
+ }
8172
+ const data = res.body.data;
8173
+ if (hasFlag("--json")) {
8174
+ console.log(JSON.stringify(data, null, 2));
8175
+ return 0;
8176
+ }
8177
+ console.log(`✓ 任务已创建(id ${data.taskId})`);
8178
+ console.log(` 首条记录已入流;追加记录:crab post ${data.taskId} --body "..."`);
8179
+ return 0;
8180
+ }
8181
+ async function collabTaskRecords(id) {
8182
+ if (!id) {
8183
+ console.error("用法:crab collab task records <id> [--limit N] [--cursor <cursor>] [--json]");
8184
+ return EXIT_USAGE;
8185
+ }
8186
+ const credentials = requireCredentials();
8187
+ if (typeof credentials === "number")
8188
+ return credentials;
8189
+ const params = new URLSearchParams;
8190
+ const limit = readFlag("--limit");
8191
+ if (limit)
8192
+ params.set("limit", limit);
8193
+ const cursor = readFlag("--cursor");
8194
+ if (cursor)
8195
+ params.set("cursor", cursor);
8196
+ const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}/records?${params}`, {
8197
+ headers: authHeaders(credentials)
8198
+ });
8199
+ if (res.status !== 200) {
8200
+ console.error(errorMessage(res.body, "读取记录流失败。"));
8201
+ return exitCodeForApiError(res.body);
8202
+ }
8203
+ const page = res.body.data;
8204
+ if (hasFlag("--json")) {
8205
+ console.log(JSON.stringify(page, null, 2));
8206
+ return 0;
8207
+ }
8208
+ if (page.records.length === 0) {
8209
+ console.log("该任务还没有记录。");
8210
+ return 0;
8211
+ }
8212
+ for (const record of page.records) {
8213
+ if (record.kind === "status_change") {
8214
+ console.log(`—— ${record.authorName ?? record.authorAccountId} ${record.actorType === "agent" ? "(经 Agent)" : ""}变更了任务状态 · ${formatTime(record.createdAt)} ——`);
8215
+ continue;
8216
+ }
8217
+ const via = record.via ? `(经 ${record.via.token})` : record.actorType === "agent" ? "(经 Agent)" : "";
8218
+ console.log(`【${record.authorName ?? record.authorAccountId}${via} · ${formatTime(record.createdAt)}】`);
8219
+ if (record.body)
8220
+ console.log(record.body);
8221
+ for (const ref of record.refs) {
8222
+ if (ref.kind === "link")
8223
+ console.log(` \uD83D\uDD17 ${ref.url}`);
8224
+ else
8225
+ console.log(` \uD83D\uDCCE ${ref.filename ?? ref.fileId}${ref.sizeBytes ? `(${formatBytesCli(ref.sizeBytes)}${ref.spaceOwned ? " · 空间文件" : " · 云盘引用"})` : ""}`);
8226
+ }
8227
+ console.log("");
8228
+ }
8229
+ if (page.nextCursor)
8230
+ console.log(`(还有更早的记录:--cursor ${page.nextCursor})`);
8231
+ return 0;
8232
+ }
8233
+ async function collabPushRecord(taskId) {
8234
+ if (!taskId) {
8235
+ console.error('用法:crab post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]');
8236
+ return EXIT_USAGE;
8237
+ }
8238
+ const bodyFlag = readFlag("--body");
8239
+ const bodyFile = readFlag("--body-file");
8240
+ if (!bodyFlag && !bodyFile) {
8241
+ console.error('需要 --body "..." 或 --body-file <path>(记录正文必填,Markdown)。');
8242
+ return EXIT_USAGE;
8243
+ }
8244
+ if (bodyFlag && bodyFile) {
8245
+ console.error("--body 与 --body-file 二选一。");
8246
+ return EXIT_USAGE;
8247
+ }
8248
+ let body = bodyFlag ?? "";
8249
+ if (bodyFile) {
8250
+ try {
8251
+ body = readFileSync2(bodyFile, "utf8");
8252
+ } catch {
8253
+ console.error(`读取正文文件失败:${bodyFile}`);
8254
+ return EXIT_USAGE;
8255
+ }
8256
+ }
8257
+ if (!body.trim()) {
8258
+ console.error("记录正文不能为空。");
8259
+ return EXIT_USAGE;
8260
+ }
8261
+ const credentials = requireCredentials();
8262
+ if (typeof credentials === "number")
8263
+ return credentials;
8264
+ const fileIds = (readFlag("--attach") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
8265
+ const links = (readFlag("--link") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
8266
+ const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(taskId)}/records`, {
8267
+ method: "POST",
8268
+ headers: authHeaders(credentials),
8269
+ body: JSON.stringify({ body, ...fileIds.length > 0 ? { fileIds } : {}, ...links.length > 0 ? { links } : {} })
8270
+ });
8271
+ if (res.status !== 200 && res.status !== 201) {
8272
+ console.error(errorMessage(res.body, "推送失败。"));
8273
+ return exitCodeForApiError(res.body);
8274
+ }
8275
+ const data = res.body.data;
8276
+ if (hasFlag("--json")) {
8277
+ console.log(JSON.stringify(data, null, 2));
8278
+ return 0;
8279
+ }
8280
+ console.log(`✓ 记录已推送(id ${data.recordId})`);
8281
+ if (data.via)
8282
+ console.log(` 活动事件 evt: ${data.via.eventId}`);
8283
+ return 0;
8284
+ }
8285
+ async function collabTaskStatus(action, id) {
8286
+ if (!id) {
8287
+ console.error(`用法:crab collab task ${action} <id>`);
8288
+ return EXIT_USAGE;
8289
+ }
8290
+ const credentials = requireCredentials();
8291
+ if (typeof credentials === "number")
8292
+ return credentials;
8293
+ const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}/${action}`, {
8294
+ method: "POST",
8295
+ headers: authHeaders(credentials)
8296
+ });
8297
+ if (res.status !== 200) {
8298
+ console.error(errorMessage(res.body, "状态更新失败。"));
8299
+ return exitCodeForApiError(res.body);
8300
+ }
8301
+ const task = res.body.data.task;
8302
+ console.log(`✓ ${action === "done" ? "已完成" : "已重开"}:${task.title}(状态变更已入记录流,记录 ${task.recordCount} 条)`);
8303
+ return 0;
8304
+ }
8305
+ async function collabTaskSummary(id) {
8306
+ if (!id) {
8307
+ console.error('用法:crab collab task summary <id> (--text "..." | --text-file <path> | --clear) [--json]');
8308
+ return EXIT_USAGE;
8309
+ }
8310
+ const text = readFlag("--text");
8311
+ const textFile = readFlag("--text-file");
8312
+ const clear = hasFlag("--clear");
8313
+ const sources = [text !== undefined, textFile !== undefined, clear].filter(Boolean).length;
8314
+ if (sources === 0) {
8315
+ console.error('需要 --text "..."(新摘要)、--text-file <path> 或 --clear(清空)之一。');
8316
+ return EXIT_USAGE;
8317
+ }
8318
+ if (sources > 1) {
8319
+ console.error("--text / --text-file / --clear 三选一。");
8320
+ return EXIT_USAGE;
8321
+ }
8322
+ let summary = text ?? "";
8323
+ if (textFile) {
8324
+ try {
8325
+ summary = readFileSync2(textFile, "utf8");
8326
+ } catch {
8327
+ console.error(`读取摘要文件失败:${textFile}`);
8328
+ return EXIT_USAGE;
8329
+ }
8330
+ }
8331
+ const credentials = requireCredentials();
8332
+ if (typeof credentials === "number")
8333
+ return credentials;
8334
+ const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}/summary`, {
8335
+ method: "PUT",
8336
+ headers: authHeaders(credentials),
8337
+ body: JSON.stringify({ summary })
8338
+ });
8339
+ if (res.status !== 200) {
8340
+ console.error(errorMessage(res.body, "摘要更新失败。"));
8341
+ return exitCodeForApiError(res.body);
8342
+ }
8343
+ const task = res.body.data.task;
8344
+ if (hasFlag("--json")) {
8345
+ console.log(JSON.stringify(task, null, 2));
8346
+ return 0;
8347
+ }
8348
+ console.log(task.summary ? `✓ 摘要已更新:${task.title}` : `✓ 摘要已清空:${task.title}`);
8349
+ if (task.summary)
8350
+ console.log(` ${task.summary.length > 200 ? `${task.summary.slice(0, 200)}…` : task.summary}`);
8351
+ return 0;
8352
+ }
8353
+ async function collabMembersList() {
8354
+ const credentials = requireCredentials();
8355
+ if (typeof credentials === "number")
8356
+ return credentials;
8357
+ const space = await resolveCollabSpace(credentials, readFlag("--space"));
8358
+ if (typeof space === "number")
8359
+ return space;
8360
+ const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/members`, {
8361
+ headers: authHeaders(credentials)
8362
+ });
8363
+ if (res.status !== 200) {
8364
+ console.error(errorMessage(res.body, "获取成员失败。"));
8365
+ return exitCodeForApiError(res.body);
8366
+ }
8367
+ const members = res.body.data.members;
8368
+ if (hasFlag("--json")) {
8369
+ console.log(JSON.stringify(members, null, 2));
8370
+ return 0;
8371
+ }
8372
+ console.log(`空间:${collabSpaceToken(space)}`);
8373
+ for (const member of members) {
8374
+ console.log(`${member.username} ${member.displayName ?? ""} [${member.role === "owner" ? "创建者" : "成员"}] 加入 ${formatTime(member.joinedAt)}`);
8375
+ }
8376
+ return 0;
8377
+ }
8378
+ async function collabInvite(username) {
8379
+ if (!username) {
8380
+ console.error("用法:crab collab invite <username> [--space <arg>] [--json]");
8381
+ return EXIT_USAGE;
8382
+ }
8383
+ const credentials = requireCredentials();
8384
+ if (typeof credentials === "number")
8385
+ return credentials;
8386
+ const space = await resolveCollabSpace(credentials, readFlag("--space"));
8387
+ if (typeof space === "number")
8388
+ return space;
8389
+ const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/members`, {
8390
+ method: "POST",
8391
+ headers: authHeaders(credentials),
8392
+ body: JSON.stringify({ username })
8393
+ });
8394
+ if (res.status !== 200 && res.status !== 201) {
8395
+ console.error(errorMessage(res.body, "邀请失败。"));
8396
+ return exitCodeForApiError(res.body);
8397
+ }
8398
+ if (hasFlag("--json")) {
8399
+ console.log(JSON.stringify({ ok: true, spaceId: space.id, username }, null, 2));
8400
+ return 0;
8401
+ }
8402
+ console.log(`✓ 已邀请 ${username} 加入 ${collabSpaceToken(space)}。`);
8403
+ return 0;
8404
+ }
8405
+ async function collabActivity() {
8406
+ const credentials = requireCredentials();
8407
+ if (typeof credentials === "number")
8408
+ return credentials;
8409
+ const res = await api2(credentials.apiBase, "/v1/collab/agent-activity", { headers: authHeaders(credentials) });
8410
+ if (res.status !== 200) {
8411
+ console.error(errorMessage(res.body, "获取协作活动失败。"));
8412
+ return exitCodeForApiError(res.body);
8413
+ }
8414
+ const activity = res.body.data.activity;
8415
+ if (hasFlag("--json")) {
8416
+ console.log(JSON.stringify(activity, null, 2));
8417
+ return 0;
8418
+ }
8419
+ if (activity.length === 0) {
8420
+ console.log("还没有协作活动事件。");
8421
+ return 0;
8422
+ }
8423
+ for (const entry of activity) {
8424
+ console.log(`${formatTime(entry.createdAt)} ${entry.action.padEnd(15)} ${entry.actorType === "agent" ? "agent" : "user "} ${entry.actorLabel ?? "—"}${entry.taskId ? ` 任务 ${entry.taskId}` : ""}`);
8425
+ }
8426
+ return 0;
8427
+ }
7577
8428
  async function init() {
7578
8429
  if (loadCredentials()) {
7579
8430
  console.log("已登录(crab whoami 查看身份)。");
@@ -7708,6 +8559,58 @@ switch (command) {
7708
8559
  console.log(usage);
7709
8560
  process.exit(EXIT_USAGE);
7710
8561
  }
8562
+ case "credits": {
8563
+ const sub = args[1];
8564
+ if (!sub || sub === "balance")
8565
+ process.exit(await creditsBalance());
8566
+ if (sub === "history")
8567
+ process.exit(await creditsHistory());
8568
+ if (sub === "pricing")
8569
+ process.exit(await creditsPricing());
8570
+ console.error(`未知子命令:credits ${sub}
8571
+ `);
8572
+ console.log(usage);
8573
+ process.exit(EXIT_USAGE);
8574
+ }
8575
+ case "collab": {
8576
+ const sub = args[1];
8577
+ if (sub === "spaces")
8578
+ process.exit(await collabSpacesList());
8579
+ if (sub === "space") {
8580
+ if (args[2] === "create")
8581
+ process.exit(await collabSpaceCreate(args.slice(3).find((x) => !x.startsWith("--"))));
8582
+ console.error(`用法:crab collab space create <名称> [--allocation-mb <兆字节>] [--json]
8583
+ `);
8584
+ process.exit(EXIT_USAGE);
8585
+ }
8586
+ if (sub === "tasks")
8587
+ process.exit(await collabTasksList());
8588
+ if (sub === "task") {
8589
+ const op = args[2];
8590
+ if (op === "create")
8591
+ process.exit(await collabTaskCreate());
8592
+ if (op === "records")
8593
+ process.exit(await collabTaskRecords(args[3]));
8594
+ if (op === "done" || op === "reopen")
8595
+ process.exit(await collabTaskStatus(op, args[3]));
8596
+ if (op === "summary")
8597
+ process.exit(await collabTaskSummary(args[3]));
8598
+ process.exit(await collabTaskDetail(op));
8599
+ }
8600
+ if (sub === "members")
8601
+ process.exit(await collabMembersList());
8602
+ if (sub === "invite")
8603
+ process.exit(await collabInvite(args[2]));
8604
+ if (sub === "activity")
8605
+ process.exit(await collabActivity());
8606
+ console.error(sub ? `未知子命令:collab ${sub}
8607
+ ` : `缺少子命令:collab spaces | space create | tasks | task | members | invite | activity
8608
+ `);
8609
+ console.log(usage);
8610
+ process.exit(EXIT_USAGE);
8611
+ }
8612
+ case "post":
8613
+ process.exit(await collabPushRecord(args[1]));
7711
8614
  case "logout":
7712
8615
  process.exit(await logout());
7713
8616
  case "version":
@@ -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
  ## 机器可读输出
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: crabcloud-collab
3
+ description: Work in the user's Crab Cloud collaboration spaces (协作空间) through the `crab` CLI — list spaces and members, create and complete tasks, push immutable execution records (Markdown + attachments/links), maintain the mutable task summary, and invite linked contacts. Use this skill when the user asks to create/update/check collaboration tasks or spaces on Crab Cloud ("建个任务", "协作空间里有什么", "把这个进度推送到任务", "邀请他进空间", "crab collab"), or wants an agent to record work progress into a task — even if they never say "crab" or "Crab Cloud" explicitly.
4
+ ---
5
+
6
+ # Crab Cloud 协作(crab collab)
7
+
8
+ 协作是用户的任务协作应用:容器是协作空间(默认个人空间注册即有 + 独立付费
9
+ 空间),原子单位是任务。任务 = 不可改的执行记录流(GitHub issue 式:什么节点
10
+ 做了什么,人机同写),另有一个可随时覆盖的「摘要」字段表达当前态概览。本
11
+ skill 覆盖协作域的 agent 通道;账号/令牌管理见 `crabcloud` skill,邮箱见
12
+ `crabcloud-mail`,云盘见 `crabcloud-storage`。
13
+
14
+ ## 第一步:确认可用
15
+
16
+ - 运行 `crab --version` 确认已安装。命令不存在时让用户运行
17
+ `npx @crabcloud/cli init`(登录 + 安装本 skill 一步完成);已装过但命令缺失
18
+ 时运行 `npx @crabcloud/cli@latest init` 升级。
19
+ - CLI 基于 npx 分发,**本机没有 `crab` 命令时,下文所有 `crab <命令>` 都可以
20
+ `npx @crabcloud/cli <命令>` 等价执行**(例如 `npx @crabcloud/cli collab tasks`)。
21
+ - 未登录(退出码 3)引导用户 `crab login`。
22
+ - **协作命令需要空间绑定**:授权时必须带 `--space`(`default` = 默认空间,
23
+ `space:<名称>` = 按名唯一命中)。含 collab scope 但没带 `--space` 会直接报
24
+ 用法错误(退出码 2)。正确示例:
25
+ `crab login --scopes collab --space default`
26
+ - 动作需要对应 scope:读(spaces/tasks/records/members)`collab.read`,写
27
+ (建任务/推送/完成/摘要/邀请)`collab.write`——一般整组授权 `--scopes collab`。
28
+ - 退出码 4 有两种协作专属情形,处置都是重新授权:
29
+ - `collab_space_not_bound`:令牌没绑空间——按上面带 `--space` 重新 `crab login`;
30
+ - `collab_space_mismatch`:令牌绑的是别的空间——换绑到目标空间(`--space`
31
+ 指对)重新授权。**不要**反复重试同一令牌。
32
+ - 推送记录免费(不消耗积分);建独立空间(5 积分)与扩容才计费,见「纪律」。
33
+
34
+ ## 空间怎么指(--space)
35
+
36
+ 所有落到一个空间的命令都接受 `--space <arg>`,`<arg>` 三种形态:
37
+
38
+ 1. `default` —— 用户的默认个人协作空间(注册即有);
39
+ 2. `space:<名称>` —— 按名称唯一命中(active 空间);
40
+ 3. 空间 id —— `crab collab spaces` 查到的 id。
41
+
42
+ **缺省 `--space` 时**:只在一个空间则自动采用;多个则 CLI 列出候选并要求显式
43
+ 指定——不要瞎猜,先 `crab collab spaces` 看一眼再带上正确的 `--space`。
44
+
45
+ ## 意图 → 命令映射
46
+
47
+ | 用户意图 | 命令 |
48
+ |---|---|
49
+ | 有哪些协作空间 | `crab collab spaces`(任务/成员计数、限额、独立空间名额) |
50
+ | 建独立协作空间 | `crab collab space create <名称> [--allocation-mb 500]`(**消耗 5 积分**,见纪律) |
51
+ | 空间里有什么任务 | `crab collab tasks [--space <arg>]`(倒序,含摘要/记录数;`--status open\|done`、`--q 关键字` 搜标题与记录正文) |
52
+ | 任务详情 | `crab collab task <id>`(状态/摘要/记录数) |
53
+ | 建任务 | `crab collab task create --space <arg> --title "..." --body "首条记录(Markdown)"`(也可 `--body-file <路径>`;附件 `--attach <fileId,...>`、链接 `--link <url,...>`) |
54
+ | 读任务的来龙去脉 | `crab collab task records <id>`(倒序记录流,Markdown 源文;`--cursor` 翻更早) |
55
+ | 推送工作记录 | `crab post <taskId> --body "做了什么(Markdown)"`(糖命令,等价 records 写入;免费) |
56
+ | 任务做完了 | `crab collab task done <id>`(入流一条状态变更);反悔 `crab collab task reopen <id>` |
57
+ | 维护任务摘要 | `crab collab task summary <id> --text "当前态概览"`(可随时覆盖;`--clear` 清空;不入记录流) |
58
+ | 谁在空间里 | `crab collab members [--space <arg>]` |
59
+ | 邀人进空间 | `crab collab invite <username> [--space <arg>]`(**只能邀联系人中已关联的平台用户**,见纪律) |
60
+ | 看协作活动 | `crab collab activity`(Agent 推送/读取、任务创建、摘要更新事件) |
61
+
62
+ 任务 id 从 `crab collab tasks` 拿;`crab post` 的 `<taskId>` 同源。
63
+
64
+ ## 机器可读输出
65
+
66
+ 程序化消费**始终加 `--json`**:
67
+
68
+ - `collab spaces --json` → `{ spaces: [...], independentLimit, independentUsed }`,
69
+ 条目含 `id / kind / name / status / taskCount / memberCount / limits`;
70
+ - `collab tasks --json` → `{ tasks: [...], nextCursor }`,条目含
71
+ `id / title / status / summary / recordCount / latestRecordAt`(游标分页);
72
+ - `collab task records --json` → `{ records: [...], nextCursor }`,条目含
73
+ `id / kind / body / authorName / via / refs / createdAt`;
74
+ - `post --json` → `{ recordId, via?: { token, eventId } }`(via.eventId 即该条
75
+ 记录 id,可确定性回查);
76
+ - `collab task <id> --json` / `collab members --json` / `collab activity --json`
77
+ 同构。
78
+
79
+ ## 纪律
80
+
81
+ - **记录流不可改**:推上去就是历史,没有编辑/删除——推送前把内容写对;写错了
82
+ 补一条更正记录,不要试图「撤回」。
83
+ - **摘要是可变当前态**:表达「现在进展到哪」,不是流水账;每次推送记录后若
84
+ 摘要已过时,顺手 `crab collab task summary` 刷新,让不看全程的人一眼看懂。
85
+ - **建独立空间是付费动作(5 积分)**:先向用户复述名称与自分配存储(默认
86
+ 500MB,占订阅总池),确认后再执行;`collab space create` 不要当作顺手操作。
87
+ - **邀请有前置**:目标必须是联系人簿里已关联的平台用户(`crab contacts add`
88
+ 站内地址自动关联);报「只能邀请联系人中已关联的平台用户」时,先引导建联
89
+ 系人,不要换名字硬试。
90
+ - **附件引用不复制**:`--attach` 的 fileId 是云盘/空间已有文件的引用(≤10 个),
91
+ 只能引用空间成员的云盘文件或本空间直传文件;跨空间文件会被 403 拒绝。
92
+ - 单任务记录流上限 500 条(状态变更也占位):任务粒度别太粗,做完就 `done`,
93
+ 新工作开新任务。
94
+ - 读记录流对 Agent 是一等审计事件(thread.read 入活动流)——正常读即可,但
95
+ 不要在循环里无意义地反复拉全量记录。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crabcloud/cli",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
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": {