@crabcloud/cli 0.1.6 → 0.1.8

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
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { hostname, homedir as homedir2 } from "node:os";
5
5
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
6
- import { join as join2 } from "node:path";
6
+ import { basename, join as join2 } from "node:path";
7
7
 
8
8
  // ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/core/util.js
9
9
  function getEnumValues(entries) {
@@ -5724,6 +5724,9 @@ function superRefine(fn, params) {
5724
5724
  var accountDomain = "crabcloud.cc";
5725
5725
  var adminDomain = `admin.${accountDomain}`;
5726
5726
  var storageDomain = `storage.${accountDomain}`;
5727
+ function storagePublicUrl(objectId) {
5728
+ return `https://${storageDomain}/o/${objectId}`;
5729
+ }
5727
5730
  var clientTypes = ["web", "agent", "miniapp", "native"];
5728
5731
  var scopes = [
5729
5732
  "account.read",
@@ -5791,7 +5794,7 @@ var registerInput = object({
5791
5794
  code: string2().trim().regex(/^\d{6}$/),
5792
5795
  displayName: string2().trim().min(1).max(80),
5793
5796
  password: string2().min(8).max(256),
5794
- invite: string2().trim().min(16).max(128).optional()
5797
+ invite: string2().trim().max(64).transform((v) => v.toUpperCase()).optional()
5795
5798
  });
5796
5799
  var loginInput = object({
5797
5800
  username: string2().trim().toLowerCase().min(3).max(254),
@@ -5829,7 +5832,8 @@ var deviceStartInput = object({
5829
5832
  var devicePollInput = object({ deviceCode: string2().min(16).max(200) });
5830
5833
  var deviceDecisionInput = object({
5831
5834
  userCode: string2().trim().min(4).max(16),
5832
- decision: _enum(["approve", "deny"])
5835
+ decision: _enum(["approve", "deny"]),
5836
+ scopes: array(_enum(scopes)).min(1).max(64).optional()
5833
5837
  });
5834
5838
  var agentTokenSummarySchema = object({
5835
5839
  id: string2(),
@@ -5939,7 +5943,8 @@ var mailSendInput = object({
5939
5943
  cc: mailRecipientList.default([]),
5940
5944
  subject: string2().trim().min(1).max(200),
5941
5945
  body: string2().min(1).max(1e5),
5942
- inReplyTo: string2().max(998).optional()
5946
+ inReplyTo: string2().max(998).optional(),
5947
+ attachmentIds: array(string2().min(1).max(64)).max(10).optional()
5943
5948
  });
5944
5949
  var mailDraftInput = object({
5945
5950
  to: mailRecipientList.default([]),
@@ -5947,11 +5952,17 @@ var mailDraftInput = object({
5947
5952
  subject: string2().trim().max(200).default(""),
5948
5953
  body: string2().max(1e5).default("")
5949
5954
  });
5955
+ var mailReadInput = object({ unread: boolean2() });
5950
5956
  var mailDevInboundInput = object({
5951
5957
  from: mailAddressSchema,
5952
5958
  to: mailAddressSchema,
5953
5959
  subject: string2().trim().min(1).max(200),
5954
- body: string2().max(1e5)
5960
+ body: string2().max(1e5),
5961
+ attachments: array(object({
5962
+ filename: string2().min(1).max(120),
5963
+ mimeType: string2().regex(/^[\w.+-]+\/[\w.+-]+$/).max(100),
5964
+ contentBase64: string2().max(35000000)
5965
+ })).max(10).optional()
5955
5966
  });
5956
5967
  var contactAddressSchema = mailAddressSchema;
5957
5968
  var contactCreateInput = object({
@@ -6201,6 +6212,7 @@ function exitCodeForApiError(body) {
6201
6212
  case "bad_request":
6202
6213
  return EXIT_USAGE;
6203
6214
  case "insufficient_credits":
6215
+ case "storage.quotaExceeded":
6204
6216
  return EXIT_QUOTA;
6205
6217
  default:
6206
6218
  return 1;
@@ -6257,8 +6269,36 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6257
6269
  mail read <id> [--json] 读取邮件正文(自动置已读)
6258
6270
  mail search <query> [--json] 全文搜索(主题/发件人/收件人/正文)
6259
6271
  mail send --to a@x.cc,b@y.cn [--cc c@z.io] --subject "..." --body "..." | --body-file <path>
6272
+ [--in-reply-to <message-id>] 回信线程锚点(原信 RFC Message-ID,crab mail read 可见)
6260
6273
  发送邮件:平台内(@crabcloud.cc)免费,外部出站 2 积分/封
6261
6274
  mail archive|trash|delete <id> 移夹 / 废纸篓 / 彻底删除(delete 不可恢复)
6275
+ contacts list [query] [--offset N] [--json] 列出联系人(地址前缀/显示名匹配)
6276
+ contacts add <address> [--name 名] [--note 备注] [--json]
6277
+ 添加联系人(站内地址自动关联对方账号)
6278
+ contacts rm <id|address> 删除联系人
6279
+ contacts invite [--reset] [--json] 我的邀请码、链接与统计(--reset 重置,旧链接失效)
6280
+ contacts invitees [--offset N] [--json]
6281
+ 我邀请的人(直接下级 + 间接计数)
6282
+ storage ls [--q 关键字] [--source files|media|mail] [--category <id|none>]
6283
+ [--type doc|image|video|archive|code] [--tag a,b] [--trash]
6284
+ [--shared] [--recent] [--limit N] [--offset N] [--json]
6285
+ 列出云盘对象(跨来源统一视图,能力与 Web 云盘一致)
6286
+ storage upload <file...> [--json] 上传一个或多个文件(直传优先,自动回退中转)
6287
+ storage download <id|文件名> [-o 输出路径]
6288
+ 下载对象内容(默认按原文件名存到当前目录)
6289
+ storage rename <id|文件名> <新名称> 重命名
6290
+ storage tag <id|文件名> --add a,b [--remove c,d]
6291
+ 增删标签
6292
+ storage category <id|文件名> <categoryId|none>
6293
+ 设置分类(分类 id 见 storage categories)
6294
+ storage publish|unpublish <id|文件名>
6295
+ 公开 / 收回公开(公开时打印公开直链)
6296
+ storage rm|restore|purge <id|文件名>
6297
+ 回收站 / 恢复 / 彻底删除(purge 不可恢复)
6298
+ storage quota [--json] 存储水位(统一容量账:文件/素材/邮件附件一本账)
6299
+ storage categories [--json] 分类列表
6300
+ storage mkcat <名称> 新建分类
6301
+ storage tags [--json] 标签汇总
6262
6302
  skills install|list|uninstall [--agent claude,zcode,codex,agents]
6263
6303
  skills 管理(claude/zcode/agents 复制目录,codex 写 AGENTS.md 段)
6264
6304
  logout 撤销当前 Agent Token 并清除本地凭证
@@ -6484,6 +6524,10 @@ async function mailRead(id) {
6484
6524
  console.log(` 收件人: ${mail.to.join(", ")}${mail.cc.length > 0 ? `
6485
6525
  抄送: ${mail.cc.join(", ")}` : ""}`);
6486
6526
  console.log(` 时间: ${formatTime(mail.createdAt)}`);
6527
+ if (mail.rfcMessageId)
6528
+ console.log(` Message-ID: ${mail.rfcMessageId}`);
6529
+ if (mail.inReplyTo)
6530
+ console.log(` 回复: ${mail.inReplyTo}`);
6487
6531
  console.log("");
6488
6532
  console.log(mail.bodyText);
6489
6533
  if (mail.actors.length > 0) {
@@ -6527,8 +6571,9 @@ async function mailSend() {
6527
6571
  const subject = readFlag("--subject") ?? "";
6528
6572
  const bodyFlag = readFlag("--body");
6529
6573
  const bodyFile = readFlag("--body-file");
6574
+ const inReplyTo = readFlag("--in-reply-to") ?? undefined;
6530
6575
  if (to.length === 0 || !subject || !bodyFlag && !bodyFile) {
6531
- console.error('用法:crab mail send --to a@x.cc[,b@y.cn] [--cc c@z.io] --subject "..." --body "..." | --body-file <path>');
6576
+ console.error('用法:crab mail send --to a@x.cc[,b@y.cn] [--cc c@z.io] --subject "..." --body "..." | --body-file <path> [--in-reply-to <message-id>]');
6532
6577
  return EXIT_USAGE;
6533
6578
  }
6534
6579
  let body = bodyFlag ?? "";
@@ -6546,7 +6591,7 @@ async function mailSend() {
6546
6591
  const res = await api2(credentials.apiBase, "/v1/mail/messages", {
6547
6592
  method: "POST",
6548
6593
  headers: authHeaders(credentials),
6549
- body: JSON.stringify({ to, cc, subject, body })
6594
+ body: JSON.stringify({ to, cc, subject, body, ...inReplyTo ? { inReplyTo } : {} })
6550
6595
  });
6551
6596
  if (res.status !== 200 && res.status !== 201) {
6552
6597
  console.error(errorMessage(res.body, "发送失败。"));
@@ -6580,6 +6625,582 @@ async function mailMove(action, id) {
6580
6625
  console.log(`✓ ${action === "archive" ? "已归档" : action === "trash" ? "已移入废纸篓" : "已彻底删除"}(${id})`);
6581
6626
  return 0;
6582
6627
  }
6628
+ function contactDisplayName(contact) {
6629
+ return contact.displayName ?? contact.resolvedName ?? contact.address;
6630
+ }
6631
+ async function contactsList(query) {
6632
+ const credentials = requireCredentials();
6633
+ if (typeof credentials === "number")
6634
+ return credentials;
6635
+ const params = new URLSearchParams;
6636
+ if (query)
6637
+ params.set("query", query);
6638
+ const offset = readFlag("--offset");
6639
+ if (offset)
6640
+ params.set("offset", offset);
6641
+ params.set("limit", readFlag("--limit") ?? "30");
6642
+ const res = await api2(credentials.apiBase, `/v1/contacts?${params}`, { headers: authHeaders(credentials) });
6643
+ if (res.status !== 200) {
6644
+ console.error(errorMessage(res.body, "获取联系人失败。"));
6645
+ return exitCodeForApiError(res.body);
6646
+ }
6647
+ const page = res.body.data;
6648
+ if (hasFlag("--json")) {
6649
+ console.log(JSON.stringify(page, null, 2));
6650
+ return 0;
6651
+ }
6652
+ if (page.items.length === 0) {
6653
+ console.log("还没有联系人。");
6654
+ return 0;
6655
+ }
6656
+ for (const contact of page.items) {
6657
+ const name = contactDisplayName(contact);
6658
+ const address = name === contact.address ? "" : ` <${contact.address}>`;
6659
+ const linked = contact.linkedUsername ? ` [站内 ${contact.linkedUsername}]` : "";
6660
+ console.log(`${contact.id} ${name}${address}${linked}`);
6661
+ }
6662
+ console.log(`共 ${page.total} 条`);
6663
+ if (page.nextOffset !== null)
6664
+ console.log(`(还有更多:contacts list --offset ${page.nextOffset})`);
6665
+ return 0;
6666
+ }
6667
+ async function contactsAdd(address) {
6668
+ if (!address) {
6669
+ console.error("用法:crab contacts add <address> [--name 显示名] [--note 备注] [--json]");
6670
+ return EXIT_USAGE;
6671
+ }
6672
+ const credentials = requireCredentials();
6673
+ if (typeof credentials === "number")
6674
+ return credentials;
6675
+ const body = { address };
6676
+ const name = readFlag("--name");
6677
+ if (name)
6678
+ body.displayName = name;
6679
+ const note = readFlag("--note");
6680
+ if (note)
6681
+ body.note = note;
6682
+ const res = await api2(credentials.apiBase, "/v1/contacts", {
6683
+ method: "POST",
6684
+ headers: authHeaders(credentials),
6685
+ body: JSON.stringify(body)
6686
+ });
6687
+ if (res.status !== 200 && res.status !== 201) {
6688
+ console.error(errorMessage(res.body, "添加失败。"));
6689
+ return exitCodeForApiError(res.body);
6690
+ }
6691
+ const contact = res.body.data;
6692
+ if (hasFlag("--json")) {
6693
+ console.log(JSON.stringify(contact, null, 2));
6694
+ return 0;
6695
+ }
6696
+ console.log(`✓ 已添加:${contactDisplayName(contact)} <${contact.address}>(id ${contact.id})`);
6697
+ return 0;
6698
+ }
6699
+ async function contactsRm(idOrAddress) {
6700
+ if (!idOrAddress) {
6701
+ console.error("用法:crab contacts rm <id|address>");
6702
+ return EXIT_USAGE;
6703
+ }
6704
+ const credentials = requireCredentials();
6705
+ if (typeof credentials === "number")
6706
+ return credentials;
6707
+ let id = idOrAddress;
6708
+ if (idOrAddress.includes("@")) {
6709
+ const res = await api2(credentials.apiBase, `/v1/contacts?query=${encodeURIComponent(idOrAddress)}&limit=50`, {
6710
+ headers: authHeaders(credentials)
6711
+ });
6712
+ if (res.status !== 200) {
6713
+ console.error(errorMessage(res.body, "查找联系人失败。"));
6714
+ return exitCodeForApiError(res.body);
6715
+ }
6716
+ const page = res.body.data;
6717
+ const hit = page.items.find((item) => item.address === idOrAddress.toLowerCase());
6718
+ if (!hit) {
6719
+ console.error("联系人不存在或已删除。");
6720
+ return EXIT_NOT_FOUND;
6721
+ }
6722
+ id = hit.id;
6723
+ }
6724
+ const res = await api2(credentials.apiBase, `/v1/contacts/${encodeURIComponent(id)}`, {
6725
+ method: "DELETE",
6726
+ headers: authHeaders(credentials)
6727
+ });
6728
+ if (res.status !== 200) {
6729
+ console.error(errorMessage(res.body, "删除失败。"));
6730
+ return exitCodeForApiError(res.body);
6731
+ }
6732
+ console.log(`✓ 已删除(${id})`);
6733
+ return 0;
6734
+ }
6735
+ async function contactsInvite(reset) {
6736
+ const credentials = requireCredentials();
6737
+ if (typeof credentials === "number")
6738
+ return credentials;
6739
+ const res = await api2(credentials.apiBase, "/v1/contacts/invite/reset", {
6740
+ method: reset ? "POST" : "GET",
6741
+ headers: authHeaders(credentials)
6742
+ });
6743
+ if (res.status !== 200) {
6744
+ console.error(errorMessage(res.body, "获取邀请链接失败。"));
6745
+ return exitCodeForApiError(res.body);
6746
+ }
6747
+ const overview = res.body.data;
6748
+ if (hasFlag("--json")) {
6749
+ console.log(JSON.stringify(overview, null, 2));
6750
+ return 0;
6751
+ }
6752
+ console.log(`邀请码:${overview.token}`);
6753
+ console.log(`邀请链接:https://crabcloud.cc/register?invite=${overview.token}`);
6754
+ console.log(`已邀请 ${overview.directCount} 人 · 含间接 ${overview.indirectCount} 人`);
6755
+ if (!reset)
6756
+ console.log("(重置链接:crab contacts invite --reset)");
6757
+ return 0;
6758
+ }
6759
+ async function contactsInvitees() {
6760
+ const credentials = requireCredentials();
6761
+ if (typeof credentials === "number")
6762
+ return credentials;
6763
+ const offset = readFlag("--offset");
6764
+ const res = await api2(credentials.apiBase, `/v1/contacts/invitees?offset=${offset ?? "0"}&limit=30`, {
6765
+ headers: authHeaders(credentials)
6766
+ });
6767
+ if (res.status !== 200) {
6768
+ console.error(errorMessage(res.body, "获取邀请列表失败。"));
6769
+ return exitCodeForApiError(res.body);
6770
+ }
6771
+ const page = res.body.data;
6772
+ if (hasFlag("--json")) {
6773
+ console.log(JSON.stringify(page, null, 2));
6774
+ return 0;
6775
+ }
6776
+ if (page.items.length === 0) {
6777
+ console.log("还没有人通过你的链接加入。");
6778
+ return 0;
6779
+ }
6780
+ for (const invitee of page.items) {
6781
+ console.log(`${invitee.username} ${invitee.displayName} ${new Date(invitee.joinedAt).toLocaleDateString("zh-CN")}`);
6782
+ }
6783
+ console.log(`直接 ${page.directTotal} · 间接 ${page.indirectTotal}`);
6784
+ if (page.nextOffset !== null)
6785
+ console.log(`(还有更多:contacts invitees --offset ${page.nextOffset})`);
6786
+ return 0;
6787
+ }
6788
+ var STORAGE_SOURCES = ["files", "media", "mail"];
6789
+ var STORAGE_TYPES = ["doc", "image", "video", "archive", "code"];
6790
+ var MIME_BY_EXT = {
6791
+ png: "image/png",
6792
+ jpg: "image/jpeg",
6793
+ jpeg: "image/jpeg",
6794
+ gif: "image/gif",
6795
+ webp: "image/webp",
6796
+ svg: "image/svg+xml",
6797
+ pdf: "application/pdf",
6798
+ txt: "text/plain",
6799
+ md: "text/markdown",
6800
+ csv: "text/csv",
6801
+ json: "application/json",
6802
+ xml: "application/xml",
6803
+ yaml: "application/x-yaml",
6804
+ yml: "application/x-yaml",
6805
+ js: "text/javascript",
6806
+ zip: "application/zip",
6807
+ gz: "application/gzip",
6808
+ mp3: "audio/mpeg",
6809
+ mp4: "video/mp4"
6810
+ };
6811
+ function mimeFor(filename) {
6812
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
6813
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
6814
+ }
6815
+ function formatBytesCli(n) {
6816
+ if (n >= 1073741824)
6817
+ return `${(n / 1073741824).toFixed(1)} GB`;
6818
+ if (n >= 1048576)
6819
+ return `${(n / 1048576).toFixed(1)} MB`;
6820
+ if (n >= 1024)
6821
+ return `${(n / 1024).toFixed(1)} KB`;
6822
+ return `${n} B`;
6823
+ }
6824
+ function printStorageRow(o) {
6825
+ const flags = [
6826
+ o.source !== "files" ? o.source === "media" ? "素材" : "邮件附件" : null,
6827
+ o.isPublic ? "公开" : null
6828
+ ].filter(Boolean);
6829
+ console.log(`${o.id} ${o.filename} ${formatBytesCli(o.sizeBytes)} ${formatTime(o.createdAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
6830
+ }
6831
+ async function resolveStorageId(credentials, token) {
6832
+ for (const trash of [false, true]) {
6833
+ const params = new URLSearchParams({ limit: "100", q: token });
6834
+ if (trash)
6835
+ params.set("trash", "1");
6836
+ const res = await api2(credentials.apiBase, `/v1/storage?${params}`, {
6837
+ headers: authHeaders(credentials)
6838
+ });
6839
+ if (res.status !== 200) {
6840
+ console.error(errorMessage(res.body, "查找对象失败。"));
6841
+ return exitCodeForApiError(res.body);
6842
+ }
6843
+ const exact = res.body.data.objects.filter((o) => o.filename === token);
6844
+ if (exact.length > 1) {
6845
+ console.error(`文件名「${token}」命中多个对象,请用 id 指定(crab storage ls 查询)。`);
6846
+ return EXIT_USAGE;
6847
+ }
6848
+ if (exact.length === 1)
6849
+ return exact[0].id;
6850
+ }
6851
+ return token;
6852
+ }
6853
+ async function storageList() {
6854
+ const credentials = requireCredentials();
6855
+ if (typeof credentials === "number")
6856
+ return credentials;
6857
+ const params = new URLSearchParams({ limit: readFlag("--limit") ?? "30" });
6858
+ const source = readFlag("--source");
6859
+ if (source) {
6860
+ if (!STORAGE_SOURCES.includes(source)) {
6861
+ console.error(`未知来源:${source}
6862
+ 可用:${STORAGE_SOURCES.join(", ")}`);
6863
+ return EXIT_USAGE;
6864
+ }
6865
+ params.set("source", source);
6866
+ }
6867
+ const type = readFlag("--type");
6868
+ if (type) {
6869
+ if (!STORAGE_TYPES.includes(type)) {
6870
+ console.error(`未知类型:${type}
6871
+ 可用:${STORAGE_TYPES.join(", ")}`);
6872
+ return EXIT_USAGE;
6873
+ }
6874
+ params.set("type", type);
6875
+ }
6876
+ const category = readFlag("--category");
6877
+ if (category)
6878
+ params.set("category", category);
6879
+ const tags = readFlag("--tag");
6880
+ if (tags)
6881
+ params.set("tags", tags);
6882
+ if (hasFlag("--trash"))
6883
+ params.set("trash", "1");
6884
+ if (hasFlag("--shared"))
6885
+ params.set("public", "1");
6886
+ if (hasFlag("--recent"))
6887
+ params.set("since", String(Date.now() - 604800000));
6888
+ const offset = readFlag("--offset");
6889
+ if (offset)
6890
+ params.set("offset", offset);
6891
+ const q = readFlag("--q");
6892
+ if (q)
6893
+ params.set("q", q);
6894
+ const res = await api2(credentials.apiBase, `/v1/storage?${params}`, { headers: authHeaders(credentials) });
6895
+ if (res.status !== 200) {
6896
+ console.error(errorMessage(res.body, "获取云盘列表失败。"));
6897
+ return exitCodeForApiError(res.body);
6898
+ }
6899
+ const page = res.body.data;
6900
+ if (hasFlag("--json")) {
6901
+ console.log(JSON.stringify(page, null, 2));
6902
+ return 0;
6903
+ }
6904
+ if (page.objects.length === 0) {
6905
+ console.log("云盘里没有匹配的对象。");
6906
+ return 0;
6907
+ }
6908
+ for (const object of page.objects)
6909
+ printStorageRow(object);
6910
+ if (page.nextOffset !== null)
6911
+ console.log(`(还有更多:storage ls --offset ${page.nextOffset})`);
6912
+ return 0;
6913
+ }
6914
+ async function storageUploadOne(credentials, filePath) {
6915
+ let buf;
6916
+ try {
6917
+ buf = readFileSync2(filePath);
6918
+ } catch {
6919
+ console.error(`读取文件失败:${filePath}`);
6920
+ return EXIT_USAGE;
6921
+ }
6922
+ const filename = basename(filePath);
6923
+ const mimeType = mimeFor(filename);
6924
+ const presign = await api2(credentials.apiBase, "/v1/storage/presign", {
6925
+ method: "POST",
6926
+ headers: authHeaders(credentials),
6927
+ body: JSON.stringify({ filename, mimeType, sizeBytes: buf.byteLength })
6928
+ });
6929
+ if (presign.status === 200) {
6930
+ const { fileId, uploadUrl } = presign.body.data;
6931
+ const put = await fetch(uploadUrl, { method: "PUT", body: new Uint8Array(buf), headers: { "content-type": mimeType } });
6932
+ if (!put.ok) {
6933
+ console.error(`直传失败(${put.status}):${filename}`);
6934
+ return 1;
6935
+ }
6936
+ const complete = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(fileId)}/complete`, {
6937
+ method: "POST",
6938
+ headers: authHeaders(credentials),
6939
+ body: "{}"
6940
+ });
6941
+ if (complete.status !== 200 && complete.status !== 201) {
6942
+ console.error(errorMessage(complete.body, `记账失败:${filename}`));
6943
+ return exitCodeForApiError(complete.body);
6944
+ }
6945
+ const object = complete.body.data;
6946
+ if (hasFlag("--json"))
6947
+ console.log(JSON.stringify(object, null, 2));
6948
+ else
6949
+ console.log(`✓ 已上传:${object.filename}(id ${object.id})`);
6950
+ return 0;
6951
+ }
6952
+ if (presign.body?.error?.code === "presign_unavailable") {
6953
+ const form = new FormData;
6954
+ form.append("file", new Blob([new Uint8Array(buf)], { type: mimeType }), filename);
6955
+ const res = await fetch(`${credentials.apiBase}/v1/storage`, {
6956
+ method: "POST",
6957
+ headers: authHeaders(credentials),
6958
+ body: form
6959
+ });
6960
+ const body = await res.json().catch(() => ({}));
6961
+ if (res.status !== 200 && res.status !== 201) {
6962
+ console.error(body.error?.message ?? `上传失败:${filename}`);
6963
+ return exitCodeForApiError(body);
6964
+ }
6965
+ if (hasFlag("--json"))
6966
+ console.log(JSON.stringify(body.data, null, 2));
6967
+ else
6968
+ console.log(`✓ 已上传:${body.data?.filename}(id ${body.data?.id})`);
6969
+ return 0;
6970
+ }
6971
+ console.error(errorMessage(presign.body, `上传失败:${filename}`));
6972
+ return exitCodeForApiError(presign.body);
6973
+ }
6974
+ async function storageUpload(paths) {
6975
+ const files = paths.filter((p) => !p.startsWith("--"));
6976
+ if (files.length === 0) {
6977
+ console.error("用法:crab storage upload <file...> [--json]");
6978
+ return EXIT_USAGE;
6979
+ }
6980
+ const credentials = requireCredentials();
6981
+ if (typeof credentials === "number")
6982
+ return credentials;
6983
+ let code = 0;
6984
+ for (const file of files) {
6985
+ const result = await storageUploadOne(credentials, file);
6986
+ if (result !== 0 && code === 0)
6987
+ code = result;
6988
+ if (result === EXIT_AUTH || result === EXIT_SCOPE)
6989
+ return result;
6990
+ }
6991
+ return code;
6992
+ }
6993
+ async function storageDownload(token) {
6994
+ if (!token) {
6995
+ console.error("用法:crab storage download <id|文件名> [-o 输出路径]");
6996
+ return EXIT_USAGE;
6997
+ }
6998
+ const credentials = requireCredentials();
6999
+ if (typeof credentials === "number")
7000
+ return credentials;
7001
+ const id = await resolveStorageId(credentials, token);
7002
+ if (typeof id === "number")
7003
+ return id;
7004
+ const res = await fetch(`${credentials.apiBase}/v1/storage/${encodeURIComponent(id)}/content`, {
7005
+ headers: authHeaders(credentials)
7006
+ });
7007
+ if (!res.ok) {
7008
+ const body = await res.json().catch(() => ({}));
7009
+ console.error(errorMessage(body, "下载失败。"));
7010
+ return exitCodeForApiError(body);
7011
+ }
7012
+ const disposition = res.headers.get("content-disposition") ?? "";
7013
+ const match = disposition.match(/filename\*=UTF-8''([^;]+)/);
7014
+ const fallbackName = match ? decodeURIComponent(match[1]) : `${id}.bin`;
7015
+ const out = readFlag("-o") ?? readFlag("--out") ?? fallbackName;
7016
+ writeFileSync2(out, Buffer.from(await res.arrayBuffer()));
7017
+ console.log(`✓ 已下载:${out}(${formatBytesCli(Number(res.headers.get("content-length") ?? 0))})`);
7018
+ return 0;
7019
+ }
7020
+ async function storagePatch(credentials, token, patch) {
7021
+ const id = await resolveStorageId(credentials, token);
7022
+ if (typeof id === "number")
7023
+ return id;
7024
+ const res = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(id)}`, {
7025
+ method: "PATCH",
7026
+ headers: authHeaders(credentials),
7027
+ body: JSON.stringify(patch)
7028
+ });
7029
+ if (res.status !== 200) {
7030
+ console.error(errorMessage(res.body, "更新失败。"));
7031
+ return exitCodeForApiError(res.body);
7032
+ }
7033
+ return res.body.data;
7034
+ }
7035
+ async function storageRename(token, name) {
7036
+ if (!token || !name) {
7037
+ console.error("用法:crab storage rename <id|文件名> <新名称>");
7038
+ return EXIT_USAGE;
7039
+ }
7040
+ const credentials = requireCredentials();
7041
+ if (typeof credentials === "number")
7042
+ return credentials;
7043
+ const result = await storagePatch(credentials, token, { filename: name });
7044
+ if (typeof result === "number")
7045
+ return result;
7046
+ console.log(`✓ 已重命名:${result.filename}(id ${result.id})`);
7047
+ return 0;
7048
+ }
7049
+ async function storageTag(token) {
7050
+ if (!token || !hasFlag("--add") && !hasFlag("--remove")) {
7051
+ console.error("用法:crab storage tag <id|文件名> --add a,b [--remove c,d]");
7052
+ return EXIT_USAGE;
7053
+ }
7054
+ const credentials = requireCredentials();
7055
+ if (typeof credentials === "number")
7056
+ return credentials;
7057
+ const add = (readFlag("--add") ?? "").split(",").map((t) => t.trim()).filter(Boolean);
7058
+ const remove = (readFlag("--remove") ?? "").split(",").map((t) => t.trim()).filter(Boolean);
7059
+ const patch = {};
7060
+ if (add.length > 0)
7061
+ patch.addTags = add;
7062
+ if (remove.length > 0)
7063
+ patch.removeTags = remove;
7064
+ const result = await storagePatch(credentials, token, patch);
7065
+ if (typeof result === "number")
7066
+ return result;
7067
+ console.log(`✓ 标签已更新:${result.filename} [${result.tags.join(", ") || "无"}]`);
7068
+ return 0;
7069
+ }
7070
+ async function storageCategory(token, value) {
7071
+ if (!token || !value) {
7072
+ console.error("用法:crab storage category <id|文件名> <categoryId|none>(分类 id 见 storage categories)");
7073
+ return EXIT_USAGE;
7074
+ }
7075
+ const credentials = requireCredentials();
7076
+ if (typeof credentials === "number")
7077
+ return credentials;
7078
+ const result = await storagePatch(credentials, token, { categoryId: value === "none" ? null : value });
7079
+ if (typeof result === "number")
7080
+ return result;
7081
+ console.log(`✓ 分类已更新:${result.filename}`);
7082
+ return 0;
7083
+ }
7084
+ async function storagePublish(token, publish) {
7085
+ if (!token) {
7086
+ console.error(`用法:crab storage ${publish ? "publish" : "unpublish"} <id|文件名>`);
7087
+ return EXIT_USAGE;
7088
+ }
7089
+ const credentials = requireCredentials();
7090
+ if (typeof credentials === "number")
7091
+ return credentials;
7092
+ const result = await storagePatch(credentials, token, { isPublic: publish });
7093
+ if (typeof result === "number")
7094
+ return result;
7095
+ console.log(`✓ ${publish ? "已公开" : "已收回公开"}:${result.filename}`);
7096
+ if (publish)
7097
+ console.log(` 公开链接:${storagePublicUrl(result.id)}`);
7098
+ return 0;
7099
+ }
7100
+ async function storageMove(action, token) {
7101
+ if (!token) {
7102
+ console.error(`用法:crab storage ${action} <id|文件名>`);
7103
+ return EXIT_USAGE;
7104
+ }
7105
+ const credentials = requireCredentials();
7106
+ if (typeof credentials === "number")
7107
+ return credentials;
7108
+ const id = await resolveStorageId(credentials, token);
7109
+ if (typeof id === "number")
7110
+ return id;
7111
+ const res = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(id)}${action === "restore" ? "/restore" : ""}${action === "purge" ? "/purge" : ""}`, { method: action === "restore" ? "POST" : "DELETE", headers: authHeaders(credentials) });
7112
+ if (res.status !== 200) {
7113
+ console.error(errorMessage(res.body, "操作失败。"));
7114
+ return exitCodeForApiError(res.body);
7115
+ }
7116
+ console.log(`✓ ${action === "rm" ? "已移入回收站(7 天后自动清理)" : action === "restore" ? "已恢复" : "已彻底删除"}(${id})`);
7117
+ return 0;
7118
+ }
7119
+ async function storageQuota() {
7120
+ const credentials = requireCredentials();
7121
+ if (typeof credentials === "number")
7122
+ return credentials;
7123
+ const res = await api2(credentials.apiBase, "/v1/storage/quota", { headers: authHeaders(credentials) });
7124
+ if (res.status !== 200) {
7125
+ console.error(errorMessage(res.body, "获取水位失败。"));
7126
+ return exitCodeForApiError(res.body);
7127
+ }
7128
+ const quota = res.body.data;
7129
+ if (hasFlag("--json")) {
7130
+ console.log(JSON.stringify(quota, null, 2));
7131
+ return 0;
7132
+ }
7133
+ const pct = quota.quotaBytes > 0 ? (quota.usedBytes / quota.quotaBytes * 100).toFixed(1) : "0";
7134
+ console.log(`已用 ${formatBytesCli(quota.usedBytes)} / ${formatBytesCli(quota.quotaBytes)}(${pct}%)`);
7135
+ console.log(` 文件 ${formatBytesCli(quota.bySource.files)} · 素材 ${formatBytesCli(quota.bySource.media)} · 邮件附件 ${formatBytesCli(quota.bySource.mail)} · 回收站 ${formatBytesCli(quota.trashBytes)}`);
7136
+ console.log(` 对象数:全部 ${quota.counts.all ?? 0} · 素材 ${quota.counts.media ?? 0} · 邮件附件 ${quota.counts.mail ?? 0} · 共享 ${quota.counts.shared ?? 0} · 回收站 ${quota.counts.trash ?? 0}`);
7137
+ return 0;
7138
+ }
7139
+ async function storageCategories() {
7140
+ const credentials = requireCredentials();
7141
+ if (typeof credentials === "number")
7142
+ return credentials;
7143
+ const res = await api2(credentials.apiBase, "/v1/storage/categories", { headers: authHeaders(credentials) });
7144
+ if (res.status !== 200) {
7145
+ console.error(errorMessage(res.body, "获取分类失败。"));
7146
+ return exitCodeForApiError(res.body);
7147
+ }
7148
+ const rows = res.body.data.categories;
7149
+ if (hasFlag("--json")) {
7150
+ console.log(JSON.stringify(rows, null, 2));
7151
+ return 0;
7152
+ }
7153
+ if (rows.length === 0) {
7154
+ console.log("还没有分类(crab storage mkcat <名称> 创建)。");
7155
+ return 0;
7156
+ }
7157
+ for (const row of rows)
7158
+ console.log(`${row.id} ${row.name} (${row.fileCount})`);
7159
+ return 0;
7160
+ }
7161
+ async function storageMkCat(name) {
7162
+ if (!name) {
7163
+ console.error("用法:crab storage mkcat <名称>");
7164
+ return EXIT_USAGE;
7165
+ }
7166
+ const credentials = requireCredentials();
7167
+ if (typeof credentials === "number")
7168
+ return credentials;
7169
+ const res = await api2(credentials.apiBase, "/v1/storage/categories", {
7170
+ method: "POST",
7171
+ headers: authHeaders(credentials),
7172
+ body: JSON.stringify({ name })
7173
+ });
7174
+ if (res.status !== 200 && res.status !== 201) {
7175
+ console.error(errorMessage(res.body, "创建分类失败。"));
7176
+ return exitCodeForApiError(res.body);
7177
+ }
7178
+ const row = res.body.data;
7179
+ console.log(`✓ 已创建分类:${row.name}(id ${row.id})`);
7180
+ return 0;
7181
+ }
7182
+ async function storageTags() {
7183
+ const credentials = requireCredentials();
7184
+ if (typeof credentials === "number")
7185
+ return credentials;
7186
+ const res = await api2(credentials.apiBase, "/v1/storage/tags", { headers: authHeaders(credentials) });
7187
+ if (res.status !== 200) {
7188
+ console.error(errorMessage(res.body, "获取标签失败。"));
7189
+ return exitCodeForApiError(res.body);
7190
+ }
7191
+ const rows = res.body.data;
7192
+ if (hasFlag("--json")) {
7193
+ console.log(JSON.stringify(rows, null, 2));
7194
+ return 0;
7195
+ }
7196
+ if (rows.tags.length === 0) {
7197
+ console.log("还没有标签(crab storage tag <id> --add a,b 添加)。");
7198
+ return 0;
7199
+ }
7200
+ for (const row of rows.tags)
7201
+ console.log(`${row.tag} (${row.count})`);
7202
+ return 0;
7203
+ }
6583
7204
  async function init() {
6584
7205
  if (loadCredentials()) {
6585
7206
  console.log("已登录(crab whoami 查看身份)。");
@@ -6652,6 +7273,58 @@ switch (command) {
6652
7273
  process.exit(await mailMove(sub, args[2]));
6653
7274
  console.error(sub ? `未知子命令:mail ${sub}
6654
7275
  ` : `缺少子命令:mail list | read | search | send | archive | trash | delete
7276
+ `);
7277
+ console.log(usage);
7278
+ process.exit(EXIT_USAGE);
7279
+ }
7280
+ case "contacts": {
7281
+ const sub = args[1];
7282
+ if (sub === "list")
7283
+ process.exit(await contactsList(args.slice(2).join(" ") || undefined));
7284
+ if (sub === "add")
7285
+ process.exit(await contactsAdd(args[2]));
7286
+ if (sub === "rm")
7287
+ process.exit(await contactsRm(args[2]));
7288
+ if (sub === "invite")
7289
+ process.exit(await contactsInvite(hasFlag("--reset")));
7290
+ if (sub === "invitees")
7291
+ process.exit(await contactsInvitees());
7292
+ console.error(sub ? `未知子命令:contacts ${sub}
7293
+ ` : `缺少子命令:contacts list | add | rm | invite | invitees
7294
+ `);
7295
+ console.log(usage);
7296
+ process.exit(EXIT_USAGE);
7297
+ }
7298
+ case "storage": {
7299
+ const sub = args[1];
7300
+ if (sub === "ls")
7301
+ process.exit(await storageList());
7302
+ if (sub === "upload")
7303
+ process.exit(await storageUpload(args.slice(2)));
7304
+ if (sub === "download")
7305
+ process.exit(await storageDownload(args[2]));
7306
+ if (sub === "rename")
7307
+ process.exit(await storageRename(args[2], args[3]));
7308
+ if (sub === "tag")
7309
+ process.exit(await storageTag(args[2]));
7310
+ if (sub === "category")
7311
+ process.exit(await storageCategory(args[2], args[3]));
7312
+ if (sub === "publish")
7313
+ process.exit(await storagePublish(args[2], true));
7314
+ if (sub === "unpublish")
7315
+ process.exit(await storagePublish(args[2], false));
7316
+ if (sub === "rm" || sub === "restore" || sub === "purge")
7317
+ process.exit(await storageMove(sub, args[2]));
7318
+ if (sub === "quota")
7319
+ process.exit(await storageQuota());
7320
+ if (sub === "categories")
7321
+ process.exit(await storageCategories());
7322
+ if (sub === "mkcat")
7323
+ process.exit(await storageMkCat(args.slice(2).join(" ") || undefined));
7324
+ if (sub === "tags")
7325
+ process.exit(await storageTags());
7326
+ console.error(sub ? `未知子命令:storage ${sub}
7327
+ ` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | tags
6655
7328
  `);
6656
7329
  console.log(usage);
6657
7330
  process.exit(EXIT_USAGE);
@@ -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", "撤销那个旧令牌"), 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 …); the Drive web app is live but has no CLI commands yet; collab and contacts ship 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 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.
4
4
  ---
5
5
 
6
6
  # Crab Cloud(平台 / 账号域)
@@ -8,9 +8,10 @@ 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` skill(`crab mail …`);
12
- 云盘已在网页上线但 **agent 通道(CLI 命令)尚未开放**,协作、联系人随后续阶段
13
- 提供——用户问到这些时如实说明,**不要猜命令**。
11
+ 本 skill 覆盖**平台、账号域与联系人**(`crab contacts …`);邮箱见
12
+ `crabcloud-mail` skill(`crab mail …`),
13
+ 云盘见 `crabcloud-storage` skill(`crab storage …`);协作随后续阶段
14
+ 提供——用户问到时如实说明,**不要猜命令**。
14
15
 
15
16
  ## 第一步:确认 CLI 可用
16
17
 
@@ -18,7 +19,9 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
18
19
  `npx @crabcloud/cli init`(登录 + 安装本 skill 一步完成);已装过但命令缺失
19
20
  时运行 `npx @crabcloud/cli@latest init` 升级。
20
21
  - `crab whoami` 报「未登录」或退出码 3 → 引导用户 `crab login`。登录走浏览器
21
- 设备授权(用户逐条确认 scope)——**你只发起,不代批**。
22
+ 设备授权——授权页摆出全目录供用户勾选,**实际授予集由人定**,可能多于或少于
23
+ `--scopes` 请求——**你只发起,不代批**;登录后先 `crab whoami` 核对实际
24
+ scopes 再干活,缺什么就引导用户带组名/scope 重新授权。
22
25
  - 永远不要让用户把 token 粘贴进对话;凭证文件与你无关,也不要读取它。
23
26
 
24
27
  ## 意图 → 命令映射
@@ -31,6 +34,9 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
31
34
  | 撤销某个令牌(高危,先确认) | `crab tokens revoke <id>` |
32
35
  | 重新授权 / 调整 scope | `crab login --scopes mail`(组名整组授权;也可单 scope 如 `mail.read`。浏览器确认,你只发起) |
33
36
  | 登出并吊销当前令牌 | `crab logout` |
37
+ | 联系人:列出 / 搜索 | `crab contacts list [query]`(`--offset`/`--limit` 分页,输出含「共 N 条」) |
38
+ | 联系人:添加 / 删除 | `crab contacts add <address> [--name 名] [--note 备注]` / `crab contacts rm <id|address>` |
39
+ | 我的邀请码、邀请链接与受邀名单 | `crab contacts invite [--reset]` / `crab contacts invitees` |
34
40
  | 管理登录设备 / 会话、修改密码 | 网页个人中心(crabcloud.cc/account)——CLI 有意不开放 |
35
41
 
36
42
  拿不准令牌 id 时先 `crab tokens list` 核对;`revoke` 只接受列表里的 id。
@@ -62,9 +68,11 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
62
68
  不要怂恿用户一次性给全量 scope。`--scopes` 支持组名(`mail` = 读写搜删四项
63
69
  整组)与单个 scope 混用;组目录见 `crab help`,可用组:account / mail /
64
70
  storage(云盘,含素材与附件)/ collab / contacts。
65
- - 如实转述能力边界:云盘已在网页上线但 agent 通道尚未开放(没有
66
- `crab storage` 等命令),协作、联系人尚未上线——相关请求如实说明,引导用户
67
- 用网页(crabcloud.cc),不要编造命令或输出;邮箱请求转交 `crabcloud-mail` skill。
71
+ - 如实转述能力边界:协作尚未上线——相关请求如实说明,引导用户
72
+ 用网页(crabcloud.cc),不要编造命令或输出;联系人已上线,用 `crab contacts`
73
+ (list / add / rm / invite / invitees;scope contacts.read/write,程序消费加
74
+ `--json`)处理;邮箱请求转交 `crabcloud-mail`
75
+ skill,云盘请求转交 `crabcloud-storage` skill。
68
76
  - 账号安全操作(修改密码、登录设备/会话的查看与吊销)**有意不开放给 agent
69
77
  通道**:改密对令牌持有者是账号接管面,会话属于"人的浏览器会话"。用户让
70
78
  agent 做这些时,如实说明并引导到网页个人中心(crabcloud.cc/account)操作,
@@ -6,8 +6,9 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
6
6
  # Crab Cloud 邮箱(crab mail)
7
7
 
8
8
  用户的 Crab Cloud 账号自带平台邮箱 `username@crabcloud.cc`(用户名即地址本地部分)。
9
- 本 skill 覆盖邮箱域的 agent 通道;账号/令牌管理见 `crabcloud` skill。云盘已在网页
10
- 上线(agent 通道未开放),协作尚未上线——用户问到时如实说明,**不要猜命令**。
9
+ 本 skill 覆盖邮箱域的 agent 通道;账号/令牌管理见 `crabcloud` skill。云盘见
10
+ `crabcloud-storage` skill(`crab storage …`),协作尚未上线——用户问到时如实说明,
11
+ **不要猜命令**。
11
12
 
12
13
  ## 第一步:确认可用
13
14
 
@@ -27,6 +28,7 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
27
28
  | 读某封邮件 | `crab mail read <id>`(自动置已读;正文 + 操作记录) |
28
29
  | 找邮件 | `crab mail search <关键词>`(主题/发件人/收件人/正文,不含废纸篓) |
29
30
  | 发邮件 | `crab mail send --to a@x.cc --subject "..." --body "..."`(长正文用 `--body-file`) |
31
+ | 回信(带线程锚点) | 先 `crab mail read <id>` 取 Message-ID,再 `crab mail send --to … --in-reply-to <message-id> …` |
30
32
  | 整理邮箱 | `crab mail archive <id>` / `crab mail trash <id>` / `crab mail delete <id>` |
31
33
 
32
34
  拿不准邮件 id 时先 `crab mail list` / `crab mail search` 核对再操作。
@@ -0,0 +1,60 @@
1
+ ---
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) through the `crab` CLI — list and filter objects, upload and download files, rename, tag, categorize, publish/unpublish public links, 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
+ ---
5
+
6
+ # Crab Cloud 云盘(crab storage)
7
+
8
+ 云盘是用户的统一存储:云盘上传 / 素材库 / 邮件附件三来源共用一本 2 GB 容量账;
9
+ 属性优先、位置无关——分类(至多一个)+ 标签(AND 检索)代替文件夹。本 skill
10
+ 覆盖云盘域的 agent 通道;账号/令牌管理见 `crabcloud` skill,邮箱见
11
+ `crabcloud-mail` skill。
12
+
13
+ ## 第一步:确认可用
14
+
15
+ - `crab --version` 确认已安装;未登录(退出码 3)引导用户 `crab login`。
16
+ - 动作需要对应 scope:列取/下载 `storage.read`,上传/改名/标签/分类/公开
17
+ `storage.write`,移回收站/恢复/彻底删除 `storage.delete`。退出码 4 = scope
18
+ 不足:如实转述缺哪个,引导 `crab login --scopes storage`(组名 = 三项整组)
19
+ 重新授权(你只发起,不代批)。
20
+ - 退出码 7 = `storage.quotaExceeded`(统一容量账满,2 GB):告知用户,可建议
21
+ 清理回收站或大文件(`storage quota` 看分项),不要自动重试。
22
+
23
+ ## 意图 → 命令映射
24
+
25
+ | 用户意图 | 命令 |
26
+ |---|---|
27
+ | 云盘里有什么 | `crab storage ls`(按时间倒序,默认 30 条) |
28
+ | 找文件 | `crab storage ls --q 关键字`(文件名匹配;可叠 `--source files\|media\|mail`、`--type doc\|image\|video\|archive\|code`、`--tag a,b`、`--shared`、`--recent`) |
29
+ | 看回收站 | `crab storage ls --trash` |
30
+ | 上传文件 | `crab storage upload <路径...>`(多文件串行;直传优先,自动回退中转) |
31
+ | 下载文件 | `crab storage download <id\|文件名> [-o 输出路径]`(默认按原文件名存当前目录) |
32
+ | 改名 | `crab storage rename <id\|文件名> <新名称>` |
33
+ | 加/删标签 | `crab storage tag <id\|文件名> --add a,b [--remove c,d]` |
34
+ | 归类 / 建分类 | `crab storage category <id\|文件名> <categoryId\|none>`;`crab storage mkcat <名称>`;分类 id 见 `crab storage categories` |
35
+ | 公开分享 | `crab storage publish <id\|文件名>`(打印公开直链;收回用 `unpublish`) |
36
+ | 删除 | `crab storage rm <id\|文件名>`(进回收站,7 天后自动清理)→ `restore` 恢复 / `purge` 彻底删除 |
37
+ | 看水位 | `crab storage quota`(三来源分项 + 回收站占用 + 对象计数) |
38
+
39
+ `<id|文件名>`:文件名全库精确唯一命中时自动解析为 id(回收站内同样可解析,
40
+ restore/purge 直接用文件名即可);命中多个或查不到时按字面当 id 用——拿不准
41
+ 先 `crab storage ls --q <关键字>` 核对再操作。
42
+
43
+ ## 机器可读输出
44
+
45
+ 程序化消费**始终加 `--json`**:
46
+
47
+ - `storage ls --json` → `{ objects: [...], nextOffset }`,条目含
48
+ `id / source / filename / mimeType / sizeBytes / categoryId / tags / isPublic / createdAt`;
49
+ - `storage quota --json` → `{ usedBytes, quotaBytes, bySource: { files, media, mail }, trashBytes, counts }`;
50
+ - `upload` / `categories` / `tags` 的 `--json` 同构(单对象 / 分类数组 / 标签数组)。
51
+
52
+ ## 纪律
53
+
54
+ - **publish 是对外公开**:公开后任何持链接者可访问,先向用户复述对象与后果,
55
+ 确认后再执行;收回用 `unpublish`(已缓存浏览器至多 5 分钟内失效)。
56
+ - **purge 是彻底删除**,不可恢复;用户没明说「彻底删除」时一律用 `rm`
57
+ (回收站保留 7 天可找回)。
58
+ - 邮件附件与用户上传同账:水位告急时用 `quota` 分项定位来源再建议清理。
59
+ - 公开直链的呈现方式由服务端策略决定(图片/视频/音频/PDF/纯文本内联,SVG 与
60
+ 其他类型强制下载),CLI 侧没有、也不需要有开关。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crabcloud/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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": {