@crabcloud/cli 0.1.10 → 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
@@ -6503,6 +6503,9 @@ function exitCodeForApiError(body) {
6503
6503
  case "insufficient_credits":
6504
6504
  case "storage.quotaExceeded":
6505
6505
  return EXIT_QUOTA;
6506
+ case "collab_space_not_bound":
6507
+ case "collab_space_mismatch":
6508
+ return EXIT_SCOPE;
6506
6509
  default:
6507
6510
  return 1;
6508
6511
  }
@@ -6550,6 +6553,9 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6550
6553
  login 浏览器授权登录(设备授权流),保存 Agent Token
6551
6554
  --scopes <scope|组名,...> 自定义 scope,支持组名整组授权
6552
6555
  (默认 ${DEFAULT_SCOPES.join(",")})
6556
+ --space <default|space:<名称>>
6557
+ 含协作 scope 时必填:令牌绑定该协作
6558
+ 空间,collab 命令只能作用于它
6553
6559
  --no-open 只打印链接,不自动打开浏览器
6554
6560
  --api <url> 指定 API 地址
6555
6561
 
@@ -6601,6 +6607,30 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
6601
6607
  storage categories [--json] 分类列表
6602
6608
  storage mkcat <名称> 新建分类
6603
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 的写入)
6604
6634
  credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
6605
6635
  credits history [--limit N] [--offset N] [--json]
6606
6636
  积分流水(变动 ±:消耗为负;需 credits.read)
@@ -6618,7 +6648,11 @@ ${scopeGroups.map((group) => ` ${group.id.padEnd(9)} ${group.scopes.join(", ")}
6618
6648
  CRAB_API API 地址(默认 ${DEFAULT_API})
6619
6649
  CRAB_CREDENTIALS 凭证文件路径(默认 ~/.config/crabcloud/credentials.json)
6620
6650
 
6621
- 退出码: 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 配额/积分不足
6622
6656
  `;
6623
6657
  async function login() {
6624
6658
  const apiBase = process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
@@ -6634,10 +6668,16 @@ async function login() {
6634
6668
  console.error(`--scopes 展开后为空(输入:${rawScopes.join(", ")})`);
6635
6669
  return EXIT_USAGE;
6636
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
+ }
6637
6677
  const openBrowser = !hasFlag("--no-open");
6638
6678
  const start = await api2(apiBase, "/v1/auth/device/start", {
6639
6679
  method: "POST",
6640
- body: JSON.stringify({ name: `${hostname()} · crab cli`, scopes: requested })
6680
+ body: JSON.stringify({ name: `${hostname()} · crab cli`, scopes: requested, ...spaceArg ? { space: spaceArg } : {} })
6641
6681
  });
6642
6682
  if (start.status !== 200) {
6643
6683
  console.error(`发起授权失败(${start.status}):`, errorMessage(start.body, JSON.stringify(start.body)));
@@ -6679,6 +6719,8 @@ async function login() {
6679
6719
  saveCredentials({ apiBase, token: poll.body.data.token, scopes: poll.body.data.scopes });
6680
6720
  console.log(`✓ 已登录,Agent Token 已保存到 ${CONFIG_PATH}`);
6681
6721
  console.log(` scopes: ${poll.body.data.scopes.join(", ")}`);
6722
+ if (spaceArg)
6723
+ console.log(` 协作空间绑定: ${spaceArg}(collab 命令只能作用于该空间)`);
6682
6724
  return 0;
6683
6725
  }
6684
6726
  }
@@ -6825,7 +6867,7 @@ async function tokensList() {
6825
6867
  return 0;
6826
6868
  }
6827
6869
  for (const token of rows) {
6828
- 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}⟩` : ""}`);
6829
6871
  console.log(` 创建 ${formatTime(token.createdAt)} · 最近使用 ${token.lastUsedAt ? formatTime(token.lastUsedAt) : "—"}${token.expiresAt ? ` · 过期 ${formatTime(token.expiresAt)}` : ""}`);
6830
6872
  }
6831
6873
  return 0;
@@ -7902,6 +7944,487 @@ async function storageTags() {
7902
7944
  console.log(`${row.tag} (${row.count})`);
7903
7945
  return 0;
7904
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
+ }
7905
8428
  async function init() {
7906
8429
  if (loadCredentials()) {
7907
8430
  console.log("已登录(crab whoami 查看身份)。");
@@ -8049,6 +8572,45 @@ switch (command) {
8049
8572
  console.log(usage);
8050
8573
  process.exit(EXIT_USAGE);
8051
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]));
8052
8614
  case "logout":
8053
8615
  process.exit(await logout());
8054
8616
  case "version":
@@ -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,8 +1,8 @@
1
1
  {
2
2
  "name": "@crabcloud/cli",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "description": "Crab Cloud CLI (crab) \u2014 agent-first entry to your personal cloud, with skills bundled for Claude Code / Codex / ZCode.",
5
+ "description": "Crab Cloud CLI (crab) — agent-first entry to your personal cloud, with skills bundled for Claude Code / Codex / ZCode.",
6
6
  "bin": {
7
7
  "crab": "dist/crab.js"
8
8
  },