@crabcloud/cli 0.1.1 → 0.1.4

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
@@ -5723,8 +5723,10 @@ function superRefine(fn, params) {
5723
5723
  // ../../packages/contracts/src/index.ts
5724
5724
  var accountDomain = "crabcloud.cc";
5725
5725
  var adminDomain = `admin.${accountDomain}`;
5726
+ var clientTypes = ["web", "agent", "miniapp", "native"];
5726
5727
  var scopes = [
5727
5728
  "account.read",
5729
+ "credits.read",
5728
5730
  "mail.read",
5729
5731
  "mail.send",
5730
5732
  "mail.search",
@@ -5732,9 +5734,29 @@ var scopes = [
5732
5734
  "files.read",
5733
5735
  "files.write",
5734
5736
  "files.delete",
5735
- "projects.read",
5736
- "projects.write"
5737
+ "contacts.read",
5738
+ "contacts.write",
5739
+ "collab.read",
5740
+ "collab.write"
5737
5741
  ];
5742
+ var scopeGroups = [
5743
+ { id: "account", scopes: ["account.read", "credits.read"] },
5744
+ { id: "mail", scopes: ["mail.read", "mail.send", "mail.search", "mail.delete"] },
5745
+ { id: "files", scopes: ["files.read", "files.write", "files.delete"] },
5746
+ { id: "collab", scopes: ["collab.read", "collab.write"] },
5747
+ { id: "contacts", scopes: ["contacts.read", "contacts.write"] }
5748
+ ];
5749
+ function expandScopes(input) {
5750
+ const expanded = input.flatMap((item) => {
5751
+ const group = scopeGroups.find((g) => g.id === item);
5752
+ return group ? [...group.scopes] : [item];
5753
+ });
5754
+ const unique = [...new Set(expanded)];
5755
+ return {
5756
+ scopes: unique.filter((scope) => scopes.includes(scope)),
5757
+ invalid: unique.filter((scope) => !scopes.includes(scope))
5758
+ };
5759
+ }
5738
5760
  var apiErrorSchema = object({
5739
5761
  code: _enum([
5740
5762
  "bad_request",
@@ -5745,6 +5767,8 @@ var apiErrorSchema = object({
5745
5767
  "conflict",
5746
5768
  "rate_limited",
5747
5769
  "expired",
5770
+ "insufficient_credits",
5771
+ "payload_too_large",
5748
5772
  "internal"
5749
5773
  ]),
5750
5774
  message: string2()
@@ -5780,6 +5804,22 @@ var passwordResetInput = object({
5780
5804
  code: string2().trim().regex(/^\d{6}$/),
5781
5805
  password: string2().min(8).max(256)
5782
5806
  });
5807
+ var profileUpdateInput = object({
5808
+ displayName: string2().trim().min(1).max(80)
5809
+ });
5810
+ var passwordChangeInput = object({
5811
+ currentPassword: string2().min(1).max(256),
5812
+ newPassword: string2().min(8).max(256)
5813
+ });
5814
+ var sessionSummarySchema = object({
5815
+ id: string2(),
5816
+ current: boolean2(),
5817
+ clientType: _enum(clientTypes),
5818
+ userAgent: string2().nullable(),
5819
+ createdAt: number2(),
5820
+ lastActiveAt: number2().nullable(),
5821
+ expiresAt: number2()
5822
+ });
5783
5823
  var deviceStartInput = object({
5784
5824
  name: string2().trim().min(1).max(80),
5785
5825
  scopes: array(_enum(scopes)).min(1).max(scopes.length)
@@ -5798,14 +5838,37 @@ var agentTokenSummarySchema = object({
5798
5838
  expiresAt: number2().nullable(),
5799
5839
  revoked: boolean2()
5800
5840
  });
5801
- var oauthProviders = ["google", "github"];
5841
+ var oauthProviders = ["google", "reai", "github"];
5802
5842
  var oauthUnbindInput = object({ provider: _enum(oauthProviders) });
5803
- var oauthOnboardingConfirmInput = object({ username: usernameSchema });
5843
+ var oauthOnboardingConfirmInput = object({
5844
+ username: usernameSchema,
5845
+ password: string2().min(8).max(256)
5846
+ });
5804
5847
  var oauthBindLoginInput = object({
5805
5848
  username: string2().trim().toLowerCase().min(3).max(254),
5806
5849
  password: string2().min(1).max(256)
5807
5850
  });
5808
5851
  var adminSettingsInput = object({ registrationOpen: boolean2() });
5852
+ var creditBuckets = ["granted", "topup"];
5853
+ var creditPackStatuses = ["draft", "live", "delisted"];
5854
+ var adminCreditAdjustInput = object({
5855
+ delta: number2().int().refine((v) => v !== 0, "delta must be a non-zero integer"),
5856
+ bucket: _enum(creditBuckets).default("topup"),
5857
+ note: string2().trim().min(1).max(500)
5858
+ });
5859
+ var creditOrderCreateInput = object({
5860
+ packId: string2().trim().min(1).max(64)
5861
+ });
5862
+ var adminCreditPackInput = object({
5863
+ title: string2().trim().min(1).max(100),
5864
+ credits: number2().int().positive().max(1e7),
5865
+ priceCents: number2().int().positive(),
5866
+ currency: string2().trim().length(3).default("CNY"),
5867
+ sort: number2().int().min(0).max(1e4).default(0)
5868
+ });
5869
+ var adminCreditPackUpdateInput = adminCreditPackInput.partial().extend({
5870
+ status: _enum(creditPackStatuses).optional()
5871
+ });
5809
5872
  var giftFulfillments = ["stock", "webhook"];
5810
5873
  var giftMerchantStatuses = ["active", "disabled"];
5811
5874
  var giftProductStatuses = ["draft", "live", "delisted"];
@@ -5816,7 +5879,6 @@ var giftCodePattern = /^[0-9A-HJ-NP-TV-Z]{16}$/;
5816
5879
  var giftCodeInput = string2().trim().min(16).max(32).transform(normalizeGiftCode).refine((code) => giftCodePattern.test(code));
5817
5880
  var giftOrderCreateInput = object({
5818
5881
  productId: string2().trim().min(1).max(64),
5819
- batchId: string2().trim().min(1).max(64).optional(),
5820
5882
  quantity: number2().int().min(1).max(10),
5821
5883
  email: backupEmailSchema
5822
5884
  });
@@ -5827,7 +5889,7 @@ var giftRedeemInput = object({
5827
5889
  externalUserId: string2().trim().max(128).optional()
5828
5890
  });
5829
5891
  var giftMerchantCreateInput = object({
5830
- id: string2().trim().toLowerCase().regex(/^[a-z][a-z0-9-]{1,31}$/),
5892
+ id: string2().trim().toLowerCase().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/),
5831
5893
  name: string2().trim().min(1).max(80),
5832
5894
  webhookUrl: string2().trim().url().max(500).optional(),
5833
5895
  note: string2().trim().max(500).optional()
@@ -5845,27 +5907,65 @@ var giftProductCreateInput = object({
5845
5907
  description: string2().trim().max(1000).optional(),
5846
5908
  metaJson: record(string2(), unknown()).optional(),
5847
5909
  emailNote: string2().trim().max(1000).optional(),
5848
- faceCents: number2().int().positive().optional(),
5849
- priceCents: number2().int().positive().optional()
5850
- }).refine((input) => input.fulfillment !== "webhook" || input.faceCents !== undefined && input.priceCents !== undefined, {
5851
- message: "webhook products require faceCents and priceCents"
5910
+ faceCents: number2().int().positive(),
5911
+ priceCents: number2().int().positive(),
5912
+ expiresAt: number2().int().positive(),
5913
+ stockQuantity: number2().int().min(0).optional()
5852
5914
  });
5853
5915
  var giftProductPatchInput = object({
5854
5916
  title: string2().trim().min(1).max(120).optional(),
5855
5917
  description: union([string2().trim().max(1000), _null3()]).optional(),
5856
5918
  metaJson: record(string2(), unknown()).optional(),
5857
5919
  emailNote: union([string2().trim().max(1000), _null3()]).optional(),
5858
- status: _enum(giftProductStatuses).optional()
5920
+ status: _enum(giftProductStatuses).optional(),
5921
+ faceCents: number2().int().positive().optional(),
5922
+ priceCents: number2().int().positive().optional(),
5923
+ expiresAt: number2().int().positive().optional(),
5924
+ stockQuantity: number2().int().min(0).optional()
5859
5925
  });
5860
- var giftBatchCreateInput = object({
5861
- productId: string2().trim().min(1).max(64),
5862
- faceCents: number2().int().positive(),
5863
- priceCents: number2().int().positive(),
5864
- count: number2().int().min(1).max(500),
5865
- expiresAt: number2().int().positive(),
5866
- note: string2().trim().max(500).optional()
5926
+ var giftProductCodesGenerateInput = object({
5927
+ count: number2().int().min(1).max(500)
5928
+ });
5929
+ var giftStockAdjustInput = object({
5930
+ delta: number2().int().refine((n) => n !== 0, { message: "delta must be non-zero" })
5867
5931
  });
5868
5932
  var giftManualCodeInput = object({ code: string2().trim().min(4).max(64) });
5933
+ var mailAddressSchema = string2().trim().toLowerCase().email().max(254);
5934
+ var mailRecipientList = array(mailAddressSchema).max(10);
5935
+ var mailSendInput = object({
5936
+ to: mailRecipientList.min(1),
5937
+ cc: mailRecipientList.default([]),
5938
+ subject: string2().trim().min(1).max(200),
5939
+ body: string2().min(1).max(1e5),
5940
+ inReplyTo: string2().max(998).optional()
5941
+ });
5942
+ var mailDraftInput = object({
5943
+ to: mailRecipientList.default([]),
5944
+ cc: mailRecipientList.default([]),
5945
+ subject: string2().trim().max(200).default(""),
5946
+ body: string2().max(1e5).default("")
5947
+ });
5948
+ var mailDevInboundInput = object({
5949
+ from: mailAddressSchema,
5950
+ to: mailAddressSchema,
5951
+ subject: string2().trim().min(1).max(200),
5952
+ body: string2().max(1e5)
5953
+ });
5954
+ var contactAddressSchema = mailAddressSchema;
5955
+ var contactCreateInput = object({
5956
+ address: contactAddressSchema,
5957
+ displayName: string2().trim().min(1).max(80).optional(),
5958
+ note: string2().trim().max(500).optional()
5959
+ });
5960
+ var contactUpdateInput = object({
5961
+ displayName: union([string2().trim().min(1).max(80), _null3()]).optional(),
5962
+ note: union([string2().trim().max(500), _null3()]).optional()
5963
+ });
5964
+ var contactQueryInput = object({
5965
+ query: string2().trim().max(100).default(""),
5966
+ limit: number2().int().min(1).max(50).default(20),
5967
+ offset: number2().int().min(0).default(0)
5968
+ });
5869
5969
 
5870
5970
  // ../../packages/skills/src/install.ts
5871
5971
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -6053,6 +6153,7 @@ var EXIT_USAGE = 2;
6053
6153
  var EXIT_AUTH = 3;
6054
6154
  var EXIT_SCOPE = 4;
6055
6155
  var EXIT_NOT_FOUND = 5;
6156
+ var EXIT_QUOTA = 7;
6056
6157
  function packageVersion() {
6057
6158
  try {
6058
6159
  const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
@@ -6095,6 +6196,10 @@ function exitCodeForApiError(body) {
6095
6196
  return EXIT_SCOPE;
6096
6197
  case "not_found":
6097
6198
  return EXIT_NOT_FOUND;
6199
+ case "bad_request":
6200
+ return EXIT_USAGE;
6201
+ case "insufficient_credits":
6202
+ return EXIT_QUOTA;
6098
6203
  default:
6099
6204
  return 1;
6100
6205
  }
@@ -6129,24 +6234,38 @@ function formatTime(ms) {
6129
6234
  return new Date(ms).toLocaleString("sv", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
6130
6235
  }
6131
6236
  var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6237
+ 发布物经 npm 分发:npx @crabcloud/cli <command>(全局安装后短名 crab 等价)
6132
6238
 
6133
6239
  用法:
6134
- crab <command> [options]
6240
+ npx @crabcloud/cli <command> [options]
6135
6241
 
6136
6242
  命令:
6137
6243
  init 登录(未登录时)+ 安装 skills 到本机 agent
6138
6244
  login 浏览器授权登录(设备授权流),保存 Agent Token
6139
- --scopes mail.read,files.write 自定义 scope(默认 ${DEFAULT_SCOPES.join(",")})
6140
- --no-open 只打印链接,不自动打开浏览器
6141
- --api <url> 指定 API 地址
6245
+ --scopes <scope|组名,...> 自定义 scope,支持组名整组授权
6246
+ (默认 ${DEFAULT_SCOPES.join(",")})
6247
+ --no-open 只打印链接,不自动打开浏览器
6248
+ --api <url> 指定 API 地址
6249
+
6142
6250
  whoami [--json] 显示当前授权身份
6143
6251
  tokens list [--json] 列出已授权的 Agent 令牌
6144
6252
  tokens revoke <id> 撤销某个令牌(高危,请先确认)
6253
+ mail list [--folder inbox|sent|drafts|archive|trash] [--unread] [--limit N] [--json]
6254
+ 列出邮件(cursor 分页契约与 Web 工作台一致)
6255
+ mail read <id> [--json] 读取邮件正文(自动置已读)
6256
+ mail search <query> [--json] 全文搜索(主题/发件人/收件人/正文)
6257
+ mail send --to a@x.cc,b@y.cn [--cc c@z.io] --subject "..." --body "..." | --body-file <path>
6258
+ 发送邮件:平台内(@crabcloud.cc)免费,外部出站 2 积分/封
6259
+ mail archive|trash|delete <id> 移夹 / 废纸篓 / 彻底删除(delete 不可恢复)
6145
6260
  skills install|list|uninstall [--agent claude,zcode,codex,agents]
6146
6261
  skills 管理(claude/zcode/agents 复制目录,codex 写 AGENTS.md 段)
6147
6262
  logout 撤销当前 Agent Token 并清除本地凭证
6148
6263
  version 显示版本
6149
6264
 
6265
+ scope 分组目录(--scopes 可混用单个 scope 与组名,组名展开为整组):
6266
+ ${scopeGroups.map((group) => ` ${group.id.padEnd(9)} ${group.scopes.join(", ")}`).join(`
6267
+ `)}
6268
+
6150
6269
  环境:
6151
6270
  CRAB_API API 地址(默认 ${DEFAULT_API})
6152
6271
  CRAB_CREDENTIALS 凭证文件路径(默认 ~/.config/crabcloud/credentials.json)
@@ -6155,11 +6274,16 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6155
6274
  `;
6156
6275
  async function login() {
6157
6276
  const apiBase = process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
6158
- const requested = (readFlag("--scopes") ?? DEFAULT_SCOPES.join(",")).split(",").map((scope) => scope.trim()).filter(Boolean);
6159
- const invalid = requested.filter((scope) => !scopes.includes(scope));
6277
+ const rawScopes = (readFlag("--scopes") ?? DEFAULT_SCOPES.join(",")).split(",").map((scope) => scope.trim()).filter(Boolean);
6278
+ const { scopes: requested, invalid } = expandScopes(rawScopes);
6160
6279
  if (invalid.length > 0) {
6161
- console.error(`未知 scope:${invalid.join(", ")}
6162
- 可用 scope:${scopes.join(", ")}`);
6280
+ console.error(`未知 scope 或组名:${invalid.join(", ")}
6281
+ 可用 scope:${scopes.join(", ")}
6282
+ 可用组名:${scopeGroups.map((group) => group.id).join(", ")}`);
6283
+ return EXIT_USAGE;
6284
+ }
6285
+ if (requested.length === 0) {
6286
+ console.error(`--scopes 展开后为空(输入:${rawScopes.join(", ")})`);
6163
6287
  return EXIT_USAGE;
6164
6288
  }
6165
6289
  const openBrowser = !hasFlag("--no-open");
@@ -6226,14 +6350,18 @@ async function whoami() {
6226
6350
  console.log(JSON.stringify(me.body.data, null, 2));
6227
6351
  return 0;
6228
6352
  }
6229
- const { account, actor, scopes: grantedScopes, personalSpace, credits } = me.body.data;
6353
+ const { account, actor, scopes: grantedScopes, personalSpace, subscription, credits, apps } = me.body.data;
6230
6354
  console.log(`${account.username}(${account.displayName})`);
6231
6355
  console.log(` 身份: ${actor}`);
6232
6356
  console.log(` scopes: ${(grantedScopes ?? []).join(", ") || "—"}`);
6233
6357
  if (personalSpace)
6234
6358
  console.log(` 个人空间: ${personalSpace.title}`);
6359
+ if (subscription)
6360
+ console.log(` 订阅: ${subscription.tier_name}`);
6235
6361
  if (credits !== undefined)
6236
6362
  console.log(` 积分余额: ${credits}`);
6363
+ if (apps?.length)
6364
+ console.log(` 已启用应用: ${apps.map((a) => a.title).join("、")}`);
6237
6365
  return 0;
6238
6366
  }
6239
6367
  async function tokensList() {
@@ -6279,6 +6407,177 @@ async function tokensRevoke(id) {
6279
6407
  console.log(`✓ 已撤销令牌 ${id}。`);
6280
6408
  return 0;
6281
6409
  }
6410
+ var MAIL_FOLDERS = ["inbox", "sent", "drafts", "archive", "trash"];
6411
+ function mailListItemName(mail) {
6412
+ return mail.folder === "sent" ? `致 ${mail.to.join(", ")}` : mail.fromName || mail.fromAddr;
6413
+ }
6414
+ function printMailRow(mail) {
6415
+ const flags = [
6416
+ mail.isUnread ? "未读" : null,
6417
+ mail.hasAttachments ? "附件" : null,
6418
+ mail.platform ? "平台" : null,
6419
+ mail.sendStatus && mail.sendStatus !== "sent" ? `发送:${mail.sendStatus}` : null
6420
+ ].filter(Boolean);
6421
+ console.log(`${mail.id} ${mailListItemName(mail)} ${mail.subject || "(无主题)"} ${formatTime(mail.createdAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
6422
+ }
6423
+ async function mailList() {
6424
+ const credentials = requireCredentials();
6425
+ if (typeof credentials === "number")
6426
+ return credentials;
6427
+ const folder = readFlag("--folder") ?? "inbox";
6428
+ if (!MAIL_FOLDERS.includes(folder)) {
6429
+ console.error(`未知文件夹:${folder}
6430
+ 可用:${MAIL_FOLDERS.join(", ")}`);
6431
+ return EXIT_USAGE;
6432
+ }
6433
+ const params = new URLSearchParams({ folder });
6434
+ if (hasFlag("--unread"))
6435
+ params.set("unreadOnly", "1");
6436
+ const limit = readFlag("--limit");
6437
+ if (limit)
6438
+ params.set("limit", limit);
6439
+ const cursor = readFlag("--cursor");
6440
+ if (cursor)
6441
+ params.set("cursor", cursor);
6442
+ const res = await api2(credentials.apiBase, `/v1/mail/messages?${params}`, { headers: authHeaders(credentials) });
6443
+ if (res.status !== 200) {
6444
+ console.error(errorMessage(res.body, "获取邮件列表失败。"));
6445
+ return exitCodeForApiError(res.body);
6446
+ }
6447
+ const page = res.body.data;
6448
+ if (hasFlag("--json")) {
6449
+ console.log(JSON.stringify(page, null, 2));
6450
+ return 0;
6451
+ }
6452
+ if (page.items.length === 0) {
6453
+ console.log("该文件夹暂时没有邮件。");
6454
+ return 0;
6455
+ }
6456
+ for (const mail of page.items)
6457
+ printMailRow(mail);
6458
+ if (page.nextCursor)
6459
+ console.log(`(还有更多:--cursor ${page.nextCursor})`);
6460
+ return 0;
6461
+ }
6462
+ async function mailRead(id) {
6463
+ if (!id) {
6464
+ console.error("用法:crab mail read <id>(id 见 crab mail list)");
6465
+ return EXIT_USAGE;
6466
+ }
6467
+ const credentials = requireCredentials();
6468
+ if (typeof credentials === "number")
6469
+ return credentials;
6470
+ const res = await api2(credentials.apiBase, `/v1/mail/messages/${encodeURIComponent(id)}`, { headers: authHeaders(credentials) });
6471
+ if (res.status !== 200) {
6472
+ console.error(errorMessage(res.body, "读取邮件失败。"));
6473
+ return exitCodeForApiError(res.body);
6474
+ }
6475
+ const mail = res.body.data.message;
6476
+ if (hasFlag("--json")) {
6477
+ console.log(JSON.stringify(mail, null, 2));
6478
+ return 0;
6479
+ }
6480
+ console.log(`${mail.subject || "(无主题)"}`);
6481
+ console.log(` 发件人: ${mail.fromName ? `${mail.fromName} <${mail.fromAddr}>` : mail.fromAddr}`);
6482
+ console.log(` 收件人: ${mail.to.join(", ")}${mail.cc.length > 0 ? `
6483
+ 抄送: ${mail.cc.join(", ")}` : ""}`);
6484
+ console.log(` 时间: ${formatTime(mail.createdAt)}`);
6485
+ console.log("");
6486
+ console.log(mail.bodyText);
6487
+ if (mail.actors.length > 0) {
6488
+ console.log("");
6489
+ console.log("操作记录:");
6490
+ for (const actor of mail.actors) {
6491
+ console.log(` ${actor.action} ${actor.actorLabel ?? "—"} ${formatTime(actor.createdAt)}`);
6492
+ }
6493
+ }
6494
+ return 0;
6495
+ }
6496
+ async function mailSearch(query) {
6497
+ if (!query || !query.trim()) {
6498
+ console.error("用法:crab mail search <query>");
6499
+ return EXIT_USAGE;
6500
+ }
6501
+ const credentials = requireCredentials();
6502
+ if (typeof credentials === "number")
6503
+ return credentials;
6504
+ const res = await api2(credentials.apiBase, `/v1/mail/search?q=${encodeURIComponent(query.trim())}`, { headers: authHeaders(credentials) });
6505
+ if (res.status !== 200) {
6506
+ console.error(errorMessage(res.body, "搜索失败。"));
6507
+ return exitCodeForApiError(res.body);
6508
+ }
6509
+ const items = res.body.data.items;
6510
+ if (hasFlag("--json")) {
6511
+ console.log(JSON.stringify(items, null, 2));
6512
+ return 0;
6513
+ }
6514
+ if (items.length === 0) {
6515
+ console.log("没有匹配的邮件。");
6516
+ return 0;
6517
+ }
6518
+ for (const mail of items)
6519
+ printMailRow(mail);
6520
+ return 0;
6521
+ }
6522
+ async function mailSend() {
6523
+ const to = (readFlag("--to") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
6524
+ const cc = (readFlag("--cc") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
6525
+ const subject = readFlag("--subject") ?? "";
6526
+ const bodyFlag = readFlag("--body");
6527
+ const bodyFile = readFlag("--body-file");
6528
+ if (to.length === 0 || !subject || !bodyFlag && !bodyFile) {
6529
+ console.error('用法:crab mail send --to a@x.cc[,b@y.cn] [--cc c@z.io] --subject "..." --body "..." | --body-file <path>');
6530
+ return EXIT_USAGE;
6531
+ }
6532
+ let body = bodyFlag ?? "";
6533
+ if (bodyFile) {
6534
+ try {
6535
+ body = readFileSync2(bodyFile, "utf8");
6536
+ } catch {
6537
+ console.error(`读取正文文件失败:${bodyFile}`);
6538
+ return EXIT_USAGE;
6539
+ }
6540
+ }
6541
+ const credentials = requireCredentials();
6542
+ if (typeof credentials === "number")
6543
+ return credentials;
6544
+ const res = await api2(credentials.apiBase, "/v1/mail/messages", {
6545
+ method: "POST",
6546
+ headers: authHeaders(credentials),
6547
+ body: JSON.stringify({ to, cc, subject, body })
6548
+ });
6549
+ if (res.status !== 200 && res.status !== 201) {
6550
+ console.error(errorMessage(res.body, "发送失败。"));
6551
+ return exitCodeForApiError(res.body);
6552
+ }
6553
+ const result = res.body.data;
6554
+ if (hasFlag("--json")) {
6555
+ console.log(JSON.stringify(result, null, 2));
6556
+ return 0;
6557
+ }
6558
+ console.log(`✓ 已发送(id ${result.message.id})`);
6559
+ if (result.chargedCredits > 0)
6560
+ console.log(` 消耗积分:${result.chargedCredits}`);
6561
+ if (result.failedExternal > 0)
6562
+ console.log(` ⚠ ${result.failedExternal} 个外部收件人发送失败,对应积分已退回`);
6563
+ return 0;
6564
+ }
6565
+ async function mailMove(action, id) {
6566
+ if (!id) {
6567
+ console.error(`用法:crab mail ${action} <id>`);
6568
+ return EXIT_USAGE;
6569
+ }
6570
+ const credentials = requireCredentials();
6571
+ if (typeof credentials === "number")
6572
+ return credentials;
6573
+ const res = await api2(credentials.apiBase, `/v1/mail/messages/${encodeURIComponent(id)}${action === "delete" ? "" : `/${action}`}`, { method: action === "delete" ? "DELETE" : "POST", headers: authHeaders(credentials) });
6574
+ if (res.status !== 200) {
6575
+ console.error(errorMessage(res.body, "操作失败。"));
6576
+ return exitCodeForApiError(res.body);
6577
+ }
6578
+ console.log(`✓ ${action === "archive" ? "已归档" : action === "trash" ? "已移入废纸篓" : "已彻底删除"}(${id})`);
6579
+ return 0;
6580
+ }
6282
6581
  async function init() {
6283
6582
  if (loadCredentials()) {
6284
6583
  console.log("已登录(crab whoami 查看身份)。");
@@ -6337,6 +6636,24 @@ switch (command) {
6337
6636
  `);
6338
6637
  process.exit(EXIT_USAGE);
6339
6638
  }
6639
+ case "mail": {
6640
+ const sub = args[1];
6641
+ if (sub === "list")
6642
+ process.exit(await mailList());
6643
+ if (sub === "read")
6644
+ process.exit(await mailRead(args[2]));
6645
+ if (sub === "search")
6646
+ process.exit(await mailSearch(args.slice(2).join(" ") || undefined));
6647
+ if (sub === "send")
6648
+ process.exit(await mailSend());
6649
+ if (sub === "archive" || sub === "trash" || sub === "delete")
6650
+ process.exit(await mailMove(sub, args[2]));
6651
+ console.error(sub ? `未知子命令:mail ${sub}
6652
+ ` : `缺少子命令:mail list | read | search | send | archive | trash | delete
6653
+ `);
6654
+ console.log(usage);
6655
+ process.exit(EXIT_USAGE);
6656
+ }
6340
6657
  case "logout":
6341
6658
  process.exit(await logout());
6342
6659
  case "version":
@@ -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. 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", "撤销那个旧令牌"), or asks how to connect their coding agent to Crab Cloud — even if they never say "crab" or "Crab Cloud" explicitly. Mail, files and projects capabilities ship with their own skills (crabcloud-mail etc.) in later phases.
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. 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", "撤销那个旧令牌"), or asks how to connect their coding agent to Crab Cloud — even if they never say "crab" or "Crab Cloud" explicitly. Email lives in the crabcloud-mail skill (crab mail …); files and projects ship in later phases.
4
4
  ---
5
5
 
6
6
  # Crab Cloud(平台 / 账号域)
@@ -8,8 +8,8 @@ description: Manage the user's Crab Cloud personal cloud account and agent token
8
8
  Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,邮箱 / 云文件 / 协作项目
9
9
  是独立应用。`crab` CLI 是 Agent 的稳定接口:凭证在用户本机
10
10
  (`~/.config/crabcloud/credentials.json`,0600),服务端只存令牌哈希、全程审计。
11
- 本 skill 目前覆盖**平台与账号域**;邮箱(`crabcloud-mail`)、云文件、协作项目
12
- 能力随各阶段上线——用户问到这些时如实说明尚未开放,**不要猜命令**。
11
+ 本 skill 覆盖**平台与账号域**;邮箱已上线,见 `crabcloud-mail` skill(`crab mail …`);
12
+ 云文件、协作项目仍随后续阶段提供——用户问到这些时如实说明尚未开放,**不要猜命令**。
13
13
 
14
14
  ## 第一步:确认 CLI 可用
15
15
 
@@ -30,6 +30,7 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
30
30
  | 撤销某个令牌(高危,先确认) | `crab tokens revoke <id>` |
31
31
  | 重新授权 / 调整 scope | `crab login --scopes mail.read`(浏览器确认,你只发起) |
32
32
  | 登出并吊销当前令牌 | `crab logout` |
33
+ | 管理登录设备 / 会话、修改密码 | 网页个人中心(crabcloud.cc/account)——CLI 有意不开放 |
33
34
 
34
35
  拿不准令牌 id 时先 `crab tokens list` 核对;`revoke` 只接受列表里的 id。
35
36
 
@@ -37,7 +38,7 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
37
38
 
38
39
  程序化消费时**始终加 `--json`**(`whoami --json` / `tokens list --json`):
39
40
 
40
- - `whoami --json` → `{ actor, scopes, account: { username, displayName, … }, personalSpace, subscription, credits }`;
41
+ - `whoami --json` → `{ actor, scopes, account: { username, displayName, … }, personalSpace, subscription, credits, apps }`;
41
42
  - `tokens list --json` → `[{ id, name, scopes, createdAt, lastUsedAt, expiresAt, revoked }]`:
42
43
  后续 `revoke` 用 `id`,向用户转述时用 `name` + `scopes`。
43
44
 
@@ -58,5 +59,9 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
58
59
  执行 `crab tokens revoke`;撤销立即生效,不可恢复(重新授权即可再建)。
59
60
  - **scope 最小化**:建议按需授权(只读场景 `crab login --scopes account.read`),
60
61
  不要怂恿用户一次性给全量 scope。
61
- - 如实转述能力边界:邮箱 / 云文件 / 协作项目尚未上线,相关请求引导用户关注
62
- 后续版本,不要编造命令或输出。
62
+ - 如实转述能力边界:云文件 / 协作项目尚未上线,相关请求引导用户关注
63
+ 后续版本,不要编造命令或输出;邮箱请求转交 `crabcloud-mail` skill。
64
+ - 账号安全操作(修改密码、登录设备/会话的查看与吊销)**有意不开放给 agent
65
+ 通道**:改密对令牌持有者是账号接管面,会话属于"人的浏览器会话"。用户让
66
+ agent 做这些时,如实说明并引导到网页个人中心(crabcloud.cc/account)操作,
67
+ 不要猜命令、也不要试图直接调 API。
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: crabcloud-mail
3
+ description: Read, search and send the user's Crab Cloud platform email (username@crabcloud.cc) through the `crab` CLI — list folders, read messages, full-text search, send mail, archive/trash/delete. Use this skill when the user asks to check, read, find, draft or send email on Crab Cloud ("看看我的邮箱", "读一下这封邮件", "帮我回复", "发邮件给…", "crab mail"), or asks what their mailbox looks like — even if they never say "crab" or "Crab Cloud" explicitly.
4
+ ---
5
+
6
+ # Crab Cloud 邮箱(crab mail)
7
+
8
+ 用户的 Crab Cloud 账号自带平台邮箱 `username@crabcloud.cc`(用户名即地址本地部分)。
9
+ 本 skill 覆盖邮箱域的 agent 通道;账号/令牌管理见 `crabcloud` skill,云文件、协作
10
+ 项目尚未开放——用户问到时如实说明,**不要猜命令**。
11
+
12
+ ## 第一步:确认可用
13
+
14
+ - `crab --version` 确认已安装;未登录(退出码 3)引导用户 `crab login`。
15
+ - 邮箱动作需要对应 scope:读 `mail.read`、搜索 `mail.search`、发信 `mail.send`、
16
+ 删除 `mail.delete`。退出码 4 = scope 不足:如实转述缺哪个,引导用户
17
+ `crab login --scopes mail.read,...` 按需最小化重新授权(你只发起,不代批)。
18
+
19
+ ## 意图 → 命令映射
20
+
21
+ | 用户意图 | 命令 |
22
+ |---|---|
23
+ | 看收件箱 / 各文件夹 | `crab mail list`(默认 inbox;`--folder sent\|drafts\|archive\|trash`) |
24
+ | 只看未读 | `crab mail list --unread` |
25
+ | 翻页 | `crab mail list --cursor <nextCursor>`(列表末尾会提示) |
26
+ | 读某封邮件 | `crab mail read <id>`(自动置已读;正文 + 操作记录) |
27
+ | 找邮件 | `crab mail search <关键词>`(主题/发件人/收件人/正文,不含废纸篓) |
28
+ | 发邮件 | `crab mail send --to a@x.cc --subject "..." --body "..."`(长正文用 `--body-file`) |
29
+ | 整理邮箱 | `crab mail archive <id>` / `crab mail trash <id>` / `crab mail delete <id>` |
30
+
31
+ 拿不准邮件 id 时先 `crab mail list` / `crab mail search` 核对再操作。
32
+
33
+ ## 计费口径(必须如实转述)
34
+
35
+ - **平台内互发免费**:收件人也是 `@crabcloud.cc` 地址时不消耗积分。
36
+ - **外部出站 2 积分/封**:发送前向用户复述收件人与计费;退出码 7 = 积分不足,
37
+ 告知用户不要自动重试。
38
+ - 发送结果会报告 `消耗积分` 与失败数;部分失败时失败部分的积分自动退回
39
+ (`发送:partial` 标记)。
40
+
41
+ ## 机器可读输出
42
+
43
+ 程序化消费**始终加 `--json`**:
44
+
45
+ - `mail list --json` → `{ items: [...], nextCursor }`,条目含
46
+ `id/folder/fromAddr/to/subject/snippet/isUnread/hasAttachments/createdAt`;
47
+ - `mail read --json` → 完整正文 + `actors`(谁读过/谁代发,审计源);
48
+ - `mail send --json` → `{ message, chargedCredits, failedExternal }`。
49
+
50
+ ## 纪律
51
+
52
+ - **发邮件是外发动作**:发送前向用户复述收件人、主题与计费口径,确认后再执行;
53
+ 不确定收件人身份时先搜索历史邮件核对,不要猜测地址。
54
+ - **`mail delete` 是彻底删除**,不可恢复;用户没明说「彻底删除」时用
55
+ `mail trash`(可找回,废纸篓 30 天后自动清除)。
56
+ - 草稿流程:先 `mail list --folder drafts` 查看草稿;本阶段 CLI 不直接改草稿,
57
+ 确认发送请把草稿内容转述给用户后用 `mail send` 发出。
58
+ - 附件:本阶段只标记 `hasAttachments`,附件内容尚未开放读取,如实说明。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crabcloud/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
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": {