@crabcloud/cli 0.1.1 → 0.1.3
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 +327 -25
- package/dist/skills/crabcloud/SKILL.md +11 -6
- package/dist/skills/crabcloud-mail/SKILL.md +58 -0
- package/package.json +1 -1
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",
|
|
5737
|
+
"media.read",
|
|
5738
|
+
"media.write",
|
|
5735
5739
|
"projects.read",
|
|
5736
5740
|
"projects.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: "media", scopes: ["media.read", "media.write"] },
|
|
5747
|
+
{ id: "projects", scopes: ["projects.read", "projects.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({
|
|
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-
|
|
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,50 @@ 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()
|
|
5849
|
-
priceCents: number2().int().positive()
|
|
5850
|
-
|
|
5851
|
-
|
|
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
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
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
|
+
});
|
|
5869
5954
|
|
|
5870
5955
|
// ../../packages/skills/src/install.ts
|
|
5871
5956
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -6053,6 +6138,7 @@ var EXIT_USAGE = 2;
|
|
|
6053
6138
|
var EXIT_AUTH = 3;
|
|
6054
6139
|
var EXIT_SCOPE = 4;
|
|
6055
6140
|
var EXIT_NOT_FOUND = 5;
|
|
6141
|
+
var EXIT_QUOTA = 7;
|
|
6056
6142
|
function packageVersion() {
|
|
6057
6143
|
try {
|
|
6058
6144
|
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -6095,6 +6181,10 @@ function exitCodeForApiError(body) {
|
|
|
6095
6181
|
return EXIT_SCOPE;
|
|
6096
6182
|
case "not_found":
|
|
6097
6183
|
return EXIT_NOT_FOUND;
|
|
6184
|
+
case "bad_request":
|
|
6185
|
+
return EXIT_USAGE;
|
|
6186
|
+
case "insufficient_credits":
|
|
6187
|
+
return EXIT_QUOTA;
|
|
6098
6188
|
default:
|
|
6099
6189
|
return 1;
|
|
6100
6190
|
}
|
|
@@ -6129,24 +6219,38 @@ function formatTime(ms) {
|
|
|
6129
6219
|
return new Date(ms).toLocaleString("sv", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
6130
6220
|
}
|
|
6131
6221
|
var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
6222
|
+
发布物经 npm 分发:npx @crabcloud/cli <command>(全局安装后短名 crab 等价)
|
|
6132
6223
|
|
|
6133
6224
|
用法:
|
|
6134
|
-
|
|
6225
|
+
npx @crabcloud/cli <command> [options]
|
|
6135
6226
|
|
|
6136
6227
|
命令:
|
|
6137
6228
|
init 登录(未登录时)+ 安装 skills 到本机 agent
|
|
6138
6229
|
login 浏览器授权登录(设备授权流),保存 Agent Token
|
|
6139
|
-
--scopes
|
|
6140
|
-
|
|
6141
|
-
--
|
|
6230
|
+
--scopes <scope|组名,...> 自定义 scope,支持组名整组授权
|
|
6231
|
+
(默认 ${DEFAULT_SCOPES.join(",")})
|
|
6232
|
+
--no-open 只打印链接,不自动打开浏览器
|
|
6233
|
+
--api <url> 指定 API 地址
|
|
6234
|
+
|
|
6142
6235
|
whoami [--json] 显示当前授权身份
|
|
6143
6236
|
tokens list [--json] 列出已授权的 Agent 令牌
|
|
6144
6237
|
tokens revoke <id> 撤销某个令牌(高危,请先确认)
|
|
6238
|
+
mail list [--folder inbox|sent|drafts|archive|trash] [--unread] [--limit N] [--json]
|
|
6239
|
+
列出邮件(cursor 分页契约与 Web 工作台一致)
|
|
6240
|
+
mail read <id> [--json] 读取邮件正文(自动置已读)
|
|
6241
|
+
mail search <query> [--json] 全文搜索(主题/发件人/收件人/正文)
|
|
6242
|
+
mail send --to a@x.cc,b@y.cn [--cc c@z.io] --subject "..." --body "..." | --body-file <path>
|
|
6243
|
+
发送邮件:平台内(@crabcloud.cc)免费,外部出站 2 积分/封
|
|
6244
|
+
mail archive|trash|delete <id> 移夹 / 废纸篓 / 彻底删除(delete 不可恢复)
|
|
6145
6245
|
skills install|list|uninstall [--agent claude,zcode,codex,agents]
|
|
6146
6246
|
skills 管理(claude/zcode/agents 复制目录,codex 写 AGENTS.md 段)
|
|
6147
6247
|
logout 撤销当前 Agent Token 并清除本地凭证
|
|
6148
6248
|
version 显示版本
|
|
6149
6249
|
|
|
6250
|
+
scope 分组目录(--scopes 可混用单个 scope 与组名,组名展开为整组):
|
|
6251
|
+
${scopeGroups.map((group) => ` ${group.id.padEnd(9)} ${group.scopes.join(", ")}`).join(`
|
|
6252
|
+
`)}
|
|
6253
|
+
|
|
6150
6254
|
环境:
|
|
6151
6255
|
CRAB_API API 地址(默认 ${DEFAULT_API})
|
|
6152
6256
|
CRAB_CREDENTIALS 凭证文件路径(默认 ~/.config/crabcloud/credentials.json)
|
|
@@ -6155,11 +6259,16 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6155
6259
|
`;
|
|
6156
6260
|
async function login() {
|
|
6157
6261
|
const apiBase = process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
|
|
6158
|
-
const
|
|
6159
|
-
const
|
|
6262
|
+
const rawScopes = (readFlag("--scopes") ?? DEFAULT_SCOPES.join(",")).split(",").map((scope) => scope.trim()).filter(Boolean);
|
|
6263
|
+
const { scopes: requested, invalid } = expandScopes(rawScopes);
|
|
6160
6264
|
if (invalid.length > 0) {
|
|
6161
|
-
console.error(`未知 scope
|
|
6162
|
-
可用 scope:${scopes.join(", ")}
|
|
6265
|
+
console.error(`未知 scope 或组名:${invalid.join(", ")}
|
|
6266
|
+
可用 scope:${scopes.join(", ")}
|
|
6267
|
+
可用组名:${scopeGroups.map((group) => group.id).join(", ")}`);
|
|
6268
|
+
return EXIT_USAGE;
|
|
6269
|
+
}
|
|
6270
|
+
if (requested.length === 0) {
|
|
6271
|
+
console.error(`--scopes 展开后为空(输入:${rawScopes.join(", ")})`);
|
|
6163
6272
|
return EXIT_USAGE;
|
|
6164
6273
|
}
|
|
6165
6274
|
const openBrowser = !hasFlag("--no-open");
|
|
@@ -6226,14 +6335,18 @@ async function whoami() {
|
|
|
6226
6335
|
console.log(JSON.stringify(me.body.data, null, 2));
|
|
6227
6336
|
return 0;
|
|
6228
6337
|
}
|
|
6229
|
-
const { account, actor, scopes: grantedScopes, personalSpace, credits } = me.body.data;
|
|
6338
|
+
const { account, actor, scopes: grantedScopes, personalSpace, subscription, credits, apps } = me.body.data;
|
|
6230
6339
|
console.log(`${account.username}(${account.displayName})`);
|
|
6231
6340
|
console.log(` 身份: ${actor}`);
|
|
6232
6341
|
console.log(` scopes: ${(grantedScopes ?? []).join(", ") || "—"}`);
|
|
6233
6342
|
if (personalSpace)
|
|
6234
6343
|
console.log(` 个人空间: ${personalSpace.title}`);
|
|
6344
|
+
if (subscription)
|
|
6345
|
+
console.log(` 订阅: ${subscription.tier_name}`);
|
|
6235
6346
|
if (credits !== undefined)
|
|
6236
6347
|
console.log(` 积分余额: ${credits}`);
|
|
6348
|
+
if (apps?.length)
|
|
6349
|
+
console.log(` 已启用应用: ${apps.map((a) => a.title).join("、")}`);
|
|
6237
6350
|
return 0;
|
|
6238
6351
|
}
|
|
6239
6352
|
async function tokensList() {
|
|
@@ -6279,6 +6392,177 @@ async function tokensRevoke(id) {
|
|
|
6279
6392
|
console.log(`✓ 已撤销令牌 ${id}。`);
|
|
6280
6393
|
return 0;
|
|
6281
6394
|
}
|
|
6395
|
+
var MAIL_FOLDERS = ["inbox", "sent", "drafts", "archive", "trash"];
|
|
6396
|
+
function mailListItemName(mail) {
|
|
6397
|
+
return mail.folder === "sent" ? `致 ${mail.to.join(", ")}` : mail.fromName || mail.fromAddr;
|
|
6398
|
+
}
|
|
6399
|
+
function printMailRow(mail) {
|
|
6400
|
+
const flags = [
|
|
6401
|
+
mail.isUnread ? "未读" : null,
|
|
6402
|
+
mail.hasAttachments ? "附件" : null,
|
|
6403
|
+
mail.platform ? "平台" : null,
|
|
6404
|
+
mail.sendStatus && mail.sendStatus !== "sent" ? `发送:${mail.sendStatus}` : null
|
|
6405
|
+
].filter(Boolean);
|
|
6406
|
+
console.log(`${mail.id} ${mailListItemName(mail)} ${mail.subject || "(无主题)"} ${formatTime(mail.createdAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
|
|
6407
|
+
}
|
|
6408
|
+
async function mailList() {
|
|
6409
|
+
const credentials = requireCredentials();
|
|
6410
|
+
if (typeof credentials === "number")
|
|
6411
|
+
return credentials;
|
|
6412
|
+
const folder = readFlag("--folder") ?? "inbox";
|
|
6413
|
+
if (!MAIL_FOLDERS.includes(folder)) {
|
|
6414
|
+
console.error(`未知文件夹:${folder}
|
|
6415
|
+
可用:${MAIL_FOLDERS.join(", ")}`);
|
|
6416
|
+
return EXIT_USAGE;
|
|
6417
|
+
}
|
|
6418
|
+
const params = new URLSearchParams({ folder });
|
|
6419
|
+
if (hasFlag("--unread"))
|
|
6420
|
+
params.set("unreadOnly", "1");
|
|
6421
|
+
const limit = readFlag("--limit");
|
|
6422
|
+
if (limit)
|
|
6423
|
+
params.set("limit", limit);
|
|
6424
|
+
const cursor = readFlag("--cursor");
|
|
6425
|
+
if (cursor)
|
|
6426
|
+
params.set("cursor", cursor);
|
|
6427
|
+
const res = await api2(credentials.apiBase, `/v1/mail/messages?${params}`, { headers: authHeaders(credentials) });
|
|
6428
|
+
if (res.status !== 200) {
|
|
6429
|
+
console.error(errorMessage(res.body, "获取邮件列表失败。"));
|
|
6430
|
+
return exitCodeForApiError(res.body);
|
|
6431
|
+
}
|
|
6432
|
+
const page = res.body.data;
|
|
6433
|
+
if (hasFlag("--json")) {
|
|
6434
|
+
console.log(JSON.stringify(page, null, 2));
|
|
6435
|
+
return 0;
|
|
6436
|
+
}
|
|
6437
|
+
if (page.items.length === 0) {
|
|
6438
|
+
console.log("该文件夹暂时没有邮件。");
|
|
6439
|
+
return 0;
|
|
6440
|
+
}
|
|
6441
|
+
for (const mail of page.items)
|
|
6442
|
+
printMailRow(mail);
|
|
6443
|
+
if (page.nextCursor)
|
|
6444
|
+
console.log(`(还有更多:--cursor ${page.nextCursor})`);
|
|
6445
|
+
return 0;
|
|
6446
|
+
}
|
|
6447
|
+
async function mailRead(id) {
|
|
6448
|
+
if (!id) {
|
|
6449
|
+
console.error("用法:crab mail read <id>(id 见 crab mail list)");
|
|
6450
|
+
return EXIT_USAGE;
|
|
6451
|
+
}
|
|
6452
|
+
const credentials = requireCredentials();
|
|
6453
|
+
if (typeof credentials === "number")
|
|
6454
|
+
return credentials;
|
|
6455
|
+
const res = await api2(credentials.apiBase, `/v1/mail/messages/${encodeURIComponent(id)}`, { headers: authHeaders(credentials) });
|
|
6456
|
+
if (res.status !== 200) {
|
|
6457
|
+
console.error(errorMessage(res.body, "读取邮件失败。"));
|
|
6458
|
+
return exitCodeForApiError(res.body);
|
|
6459
|
+
}
|
|
6460
|
+
const mail = res.body.data.message;
|
|
6461
|
+
if (hasFlag("--json")) {
|
|
6462
|
+
console.log(JSON.stringify(mail, null, 2));
|
|
6463
|
+
return 0;
|
|
6464
|
+
}
|
|
6465
|
+
console.log(`${mail.subject || "(无主题)"}`);
|
|
6466
|
+
console.log(` 发件人: ${mail.fromName ? `${mail.fromName} <${mail.fromAddr}>` : mail.fromAddr}`);
|
|
6467
|
+
console.log(` 收件人: ${mail.to.join(", ")}${mail.cc.length > 0 ? `
|
|
6468
|
+
抄送: ${mail.cc.join(", ")}` : ""}`);
|
|
6469
|
+
console.log(` 时间: ${formatTime(mail.createdAt)}`);
|
|
6470
|
+
console.log("");
|
|
6471
|
+
console.log(mail.bodyText);
|
|
6472
|
+
if (mail.actors.length > 0) {
|
|
6473
|
+
console.log("");
|
|
6474
|
+
console.log("操作记录:");
|
|
6475
|
+
for (const actor of mail.actors) {
|
|
6476
|
+
console.log(` ${actor.action} ${actor.actorLabel ?? "—"} ${formatTime(actor.createdAt)}`);
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
return 0;
|
|
6480
|
+
}
|
|
6481
|
+
async function mailSearch(query) {
|
|
6482
|
+
if (!query || !query.trim()) {
|
|
6483
|
+
console.error("用法:crab mail search <query>");
|
|
6484
|
+
return EXIT_USAGE;
|
|
6485
|
+
}
|
|
6486
|
+
const credentials = requireCredentials();
|
|
6487
|
+
if (typeof credentials === "number")
|
|
6488
|
+
return credentials;
|
|
6489
|
+
const res = await api2(credentials.apiBase, `/v1/mail/search?q=${encodeURIComponent(query.trim())}`, { headers: authHeaders(credentials) });
|
|
6490
|
+
if (res.status !== 200) {
|
|
6491
|
+
console.error(errorMessage(res.body, "搜索失败。"));
|
|
6492
|
+
return exitCodeForApiError(res.body);
|
|
6493
|
+
}
|
|
6494
|
+
const items = res.body.data.items;
|
|
6495
|
+
if (hasFlag("--json")) {
|
|
6496
|
+
console.log(JSON.stringify(items, null, 2));
|
|
6497
|
+
return 0;
|
|
6498
|
+
}
|
|
6499
|
+
if (items.length === 0) {
|
|
6500
|
+
console.log("没有匹配的邮件。");
|
|
6501
|
+
return 0;
|
|
6502
|
+
}
|
|
6503
|
+
for (const mail of items)
|
|
6504
|
+
printMailRow(mail);
|
|
6505
|
+
return 0;
|
|
6506
|
+
}
|
|
6507
|
+
async function mailSend() {
|
|
6508
|
+
const to = (readFlag("--to") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
6509
|
+
const cc = (readFlag("--cc") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
6510
|
+
const subject = readFlag("--subject") ?? "";
|
|
6511
|
+
const bodyFlag = readFlag("--body");
|
|
6512
|
+
const bodyFile = readFlag("--body-file");
|
|
6513
|
+
if (to.length === 0 || !subject || !bodyFlag && !bodyFile) {
|
|
6514
|
+
console.error('用法:crab mail send --to a@x.cc[,b@y.cn] [--cc c@z.io] --subject "..." --body "..." | --body-file <path>');
|
|
6515
|
+
return EXIT_USAGE;
|
|
6516
|
+
}
|
|
6517
|
+
let body = bodyFlag ?? "";
|
|
6518
|
+
if (bodyFile) {
|
|
6519
|
+
try {
|
|
6520
|
+
body = readFileSync2(bodyFile, "utf8");
|
|
6521
|
+
} catch {
|
|
6522
|
+
console.error(`读取正文文件失败:${bodyFile}`);
|
|
6523
|
+
return EXIT_USAGE;
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6526
|
+
const credentials = requireCredentials();
|
|
6527
|
+
if (typeof credentials === "number")
|
|
6528
|
+
return credentials;
|
|
6529
|
+
const res = await api2(credentials.apiBase, "/v1/mail/messages", {
|
|
6530
|
+
method: "POST",
|
|
6531
|
+
headers: authHeaders(credentials),
|
|
6532
|
+
body: JSON.stringify({ to, cc, subject, body })
|
|
6533
|
+
});
|
|
6534
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
6535
|
+
console.error(errorMessage(res.body, "发送失败。"));
|
|
6536
|
+
return exitCodeForApiError(res.body);
|
|
6537
|
+
}
|
|
6538
|
+
const result = res.body.data;
|
|
6539
|
+
if (hasFlag("--json")) {
|
|
6540
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6541
|
+
return 0;
|
|
6542
|
+
}
|
|
6543
|
+
console.log(`✓ 已发送(id ${result.message.id})`);
|
|
6544
|
+
if (result.chargedCredits > 0)
|
|
6545
|
+
console.log(` 消耗积分:${result.chargedCredits}`);
|
|
6546
|
+
if (result.failedExternal > 0)
|
|
6547
|
+
console.log(` ⚠ ${result.failedExternal} 个外部收件人发送失败,对应积分已退回`);
|
|
6548
|
+
return 0;
|
|
6549
|
+
}
|
|
6550
|
+
async function mailMove(action, id) {
|
|
6551
|
+
if (!id) {
|
|
6552
|
+
console.error(`用法:crab mail ${action} <id>`);
|
|
6553
|
+
return EXIT_USAGE;
|
|
6554
|
+
}
|
|
6555
|
+
const credentials = requireCredentials();
|
|
6556
|
+
if (typeof credentials === "number")
|
|
6557
|
+
return credentials;
|
|
6558
|
+
const res = await api2(credentials.apiBase, `/v1/mail/messages/${encodeURIComponent(id)}${action === "delete" ? "" : `/${action}`}`, { method: action === "delete" ? "DELETE" : "POST", headers: authHeaders(credentials) });
|
|
6559
|
+
if (res.status !== 200) {
|
|
6560
|
+
console.error(errorMessage(res.body, "操作失败。"));
|
|
6561
|
+
return exitCodeForApiError(res.body);
|
|
6562
|
+
}
|
|
6563
|
+
console.log(`✓ ${action === "archive" ? "已归档" : action === "trash" ? "已移入废纸篓" : "已彻底删除"}(${id})`);
|
|
6564
|
+
return 0;
|
|
6565
|
+
}
|
|
6282
6566
|
async function init() {
|
|
6283
6567
|
if (loadCredentials()) {
|
|
6284
6568
|
console.log("已登录(crab whoami 查看身份)。");
|
|
@@ -6337,6 +6621,24 @@ switch (command) {
|
|
|
6337
6621
|
`);
|
|
6338
6622
|
process.exit(EXIT_USAGE);
|
|
6339
6623
|
}
|
|
6624
|
+
case "mail": {
|
|
6625
|
+
const sub = args[1];
|
|
6626
|
+
if (sub === "list")
|
|
6627
|
+
process.exit(await mailList());
|
|
6628
|
+
if (sub === "read")
|
|
6629
|
+
process.exit(await mailRead(args[2]));
|
|
6630
|
+
if (sub === "search")
|
|
6631
|
+
process.exit(await mailSearch(args.slice(2).join(" ") || undefined));
|
|
6632
|
+
if (sub === "send")
|
|
6633
|
+
process.exit(await mailSend());
|
|
6634
|
+
if (sub === "archive" || sub === "trash" || sub === "delete")
|
|
6635
|
+
process.exit(await mailMove(sub, args[2]));
|
|
6636
|
+
console.error(sub ? `未知子命令:mail ${sub}
|
|
6637
|
+
` : `缺少子命令:mail list | read | search | send | archive | trash | delete
|
|
6638
|
+
`);
|
|
6639
|
+
console.log(usage);
|
|
6640
|
+
process.exit(EXIT_USAGE);
|
|
6641
|
+
}
|
|
6340
6642
|
case "logout":
|
|
6341
6643
|
process.exit(await logout());
|
|
6342
6644
|
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.
|
|
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
|
|
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