@yijiancaotang/cli 0.0.0-stage → 1.1.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/README.md +311 -2
- package/bin/caotang.cjs +15 -0
- package/dist/agent-identity.js +14 -0
- package/dist/auth.js +340 -0
- package/dist/calendar-contract.js +358 -0
- package/dist/calendar-help.js +12 -0
- package/dist/calendar.js +64 -0
- package/dist/capability-result.js +85 -0
- package/dist/cli.js +257 -0
- package/dist/command-help.js +105 -0
- package/dist/commands.js +356 -0
- package/dist/config.js +14 -0
- package/dist/connection-copy.js +40 -0
- package/dist/context.js +30 -0
- package/dist/contracts.js +1 -0
- package/dist/create-bundle.js +103 -0
- package/dist/create-file-failure.js +18 -0
- package/dist/credential-store.js +594 -0
- package/dist/doctor.js +221 -0
- package/dist/errors.js +214 -0
- package/dist/generated/knowledge-growth.js +37 -0
- package/dist/habit-day.js +58 -0
- package/dist/habit-read.js +32 -0
- package/dist/habit-wire.js +87 -0
- package/dist/http.js +211 -0
- package/dist/input-validation.js +76 -0
- package/dist/installation.js +761 -0
- package/dist/knowledge-growth.js +85 -0
- package/dist/local-commands.js +229 -0
- package/dist/local-lock.js +245 -0
- package/dist/local-records.js +149 -0
- package/dist/local-types.js +3 -0
- package/dist/managed-skill-baseline.js +35 -0
- package/dist/missing-sections.js +72 -0
- package/dist/onboarding.js +48 -0
- package/dist/output.js +54 -0
- package/dist/pending-requests.js +45 -0
- package/dist/platforms.js +239 -0
- package/dist/query-daily.js +70 -0
- package/dist/query-search.js +43 -0
- package/dist/query-validation.js +45 -0
- package/dist/reviewed-managed-bundle-baseline.js +356 -0
- package/dist/schedule-contract.js +289 -0
- package/dist/schedule-help.js +35 -0
- package/dist/schedule-start-wire.js +80 -0
- package/dist/schedule.js +70 -0
- package/dist/skill-assets.js +136 -0
- package/dist/upload.js +130 -0
- package/dist/web-citations.js +68 -0
- package/dist/write-permissions.js +41 -0
- package/dist/write-routing.js +26 -0
- package/package.json +27 -4
- package/skills/caotang-context/SKILL.md +52 -0
- package/skills/caotang-first-connect/SKILL.md +74 -0
- package/skills/caotang-project-progress/SKILL.md +91 -0
- package/skills/caotang-save-knowledge/SKILL.md +42 -0
- package/skills/caotang-schedule/SKILL.md +54 -0
- package/skills/caotang-search/SKILL.md +79 -0
- package/skills/caotang-workday/SKILL.md +81 -0
- package/skills/manifest.json +33 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { dailySummaryResult } from "./calendar-contract.js";
|
|
2
|
+
import { validDate as validDay } from "./query-validation.js";
|
|
3
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
4
|
+
const list = (value, n) => { if (!Array.isArray(value) || value.length > n)
|
|
5
|
+
throw Error(); return value; };
|
|
6
|
+
const str = (v, max) => { if (typeof v !== "string" || [...v].length > max || v.includes("\0"))
|
|
7
|
+
throw Error(); return v; };
|
|
8
|
+
const id = (v) => { const s = str(v, 128); if (!/^[a-z0-9_-]+(?![\s\S])/i.test(s))
|
|
9
|
+
throw Error(); return s; };
|
|
10
|
+
const key = (v) => { const s = str(v, 64); if (!/^[a-z][a-z0-9_]*(?![\s\S])/.test(s))
|
|
11
|
+
throw Error(); return s; };
|
|
12
|
+
const count = (v) => { if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0)
|
|
13
|
+
throw Error(); return v; };
|
|
14
|
+
function rule(value) {
|
|
15
|
+
const r = record(value), text = str(r.text, 200);
|
|
16
|
+
// The Go writer rejects controls before trimming Unicode White_Space.
|
|
17
|
+
// A literal BOM is content, while NEL / lone surrogates cannot be stored.
|
|
18
|
+
if (/[\p{Cc}\u2028\u2029\uD800-\uDFFF]/u.test(text) || !text || text.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "") !== text)
|
|
19
|
+
throw Error();
|
|
20
|
+
return { id: id(r.id), text, created_at: str(r.created_at, 64), updated_at: str(r.updated_at, 64) };
|
|
21
|
+
}
|
|
22
|
+
function pending(value) { const r = record(value), total = count(r.total), items = list(r.items, 3).map(value => { const p = record(value); return { id: id(p.id), action_type: key(p.action_type), summary: str(p.summary, 120) }; }); if (items.length > total)
|
|
23
|
+
throw Error(); return { total, items }; }
|
|
24
|
+
function day(value) { const r = record(value); if (!validDay(r.date))
|
|
25
|
+
throw Error(); const total = count(r.total), items = list(r.items, 3).map(value => { const p = record(value); if (p.kind !== "project_update" && p.kind !== "checkin")
|
|
26
|
+
throw Error(); return { kind: p.kind, id: id(p.id), summary: str(p.summary, 120), ...(p.project_id === undefined ? {} : { project_id: id(p.project_id) }) }; }); if (items.length > total)
|
|
27
|
+
throw Error(); return { date: r.date, total, items }; }
|
|
28
|
+
function step(value) { const r = record(value); return { id: id(r.id), project_id: id(r.project_id), summary: str(r.summary, 120), created_at: str(r.created_at, 64) }; }
|
|
29
|
+
function gap(value) { const r = record(value), section = key(r.section); if (typeof r.reason !== "string" || !["capability_disabled", "read_failed", "community_disabled", "record_failed"].includes(r.reason))
|
|
30
|
+
throw Error(); if (r.reason === "record_failed" && (section !== "calendar_start" || r.required_capability !== "read_workbench"))
|
|
31
|
+
throw Error(); if (section === "calendar_start" && (r.required_capability !== "read_workbench" || !["capability_disabled", "record_failed"].includes(r.reason)))
|
|
32
|
+
throw Error(); return { section, ...(r.required_capability === undefined ? {} : { required_capability: key(r.required_capability) }), reason: r.reason }; }
|
|
33
|
+
function official(value) { const r = record(value), total = count(r.unread_count), title = str(r.latest_title, 120); if ((total === 0 && title !== "") || (total > 0 && !title))
|
|
34
|
+
throw Error(); return { unread_count: total, latest_title: title }; }
|
|
35
|
+
// Project the bounded wire fields rather than forwarding unknown content or
|
|
36
|
+
// treating a missing section as empty. Unknown account truth wins over empty.
|
|
37
|
+
export function verifiedHabit(value, start) {
|
|
38
|
+
try {
|
|
39
|
+
const r = record(value);
|
|
40
|
+
if (!start) {
|
|
41
|
+
if (r.limit !== 20 || r.max_chars !== 200)
|
|
42
|
+
return;
|
|
43
|
+
return { items: list(r.items, 20).map(rule), limit: 20, max_chars: 200 };
|
|
44
|
+
}
|
|
45
|
+
if (typeof r.partial !== "boolean" || !(typeof r.account_empty === "boolean" || r.account_empty === null))
|
|
46
|
+
return;
|
|
47
|
+
const rules = r.rules === null ? null : list(r.rules, 20).map(rule), p = r.pending === null ? null : pending(r.pending), days = r.recent_days === null ? null : list(r.recent_days, 3).map(day), next = r.next_step === null ? null : step(r.next_step), gaps = list(r.missing_sections, 32).map(gap);
|
|
48
|
+
const owner = typeof r.owner_id === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}(?![\s\S])/i.test(r.owner_id) ? r.owner_id : undefined;
|
|
49
|
+
const schedules = r.schedule_items === undefined ? undefined : r.schedule_items === null ? null : verifiedScheduleStart(r.schedule_items);
|
|
50
|
+
const summary = r.last_daily_summary === undefined ? undefined : r.last_daily_summary === null ? null : dailySummaryResult(r.last_daily_summary);
|
|
51
|
+
const incomplete = rules === null || p === null || days === null || schedules === null || gaps.some(g => g.reason !== "community_disabled" && g.reason !== "record_failed");
|
|
52
|
+
const onlyStartRecordGap = gaps.length > 0 && gaps.every(g => g.section === "calendar_start" && g.reason === "record_failed");
|
|
53
|
+
if ((incomplete || gaps.length > 0) && !r.partial)
|
|
54
|
+
return;
|
|
55
|
+
const contradicted = r.account_empty === true && (!!rules?.length || !!p?.total || !!days?.length || !!next || !!schedules?.total_stored || !!summary);
|
|
56
|
+
return { ...(owner ? { owner_id: owner } : {}), ...(r.official_notices === undefined ? {} : { official_notices: r.official_notices === null ? null : official(r.official_notices) }), rules, pending: p, recent_days: days, next_step: next,
|
|
57
|
+
...(schedules === undefined ? {} : { schedule_items: schedules }), ...(summary === undefined ? {} : { last_daily_summary: summary }), account_empty: !owner || (r.partial && !onlyStartRecordGap) || incomplete || contradicted ? null : r.account_empty, partial: r.partial, missing_sections: gaps };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
import { verifiedScheduleStart } from "./schedule-start-wire.js";
|
|
64
|
+
// A closed content switch is diagnosed only from an exact, non-conflicting
|
|
65
|
+
// start receipt; bare 403 and unrelated capabilities remain unknown.
|
|
66
|
+
export function habitWorkbenchDisabled(value) {
|
|
67
|
+
const body = record(value);
|
|
68
|
+
if (!body)
|
|
69
|
+
return false;
|
|
70
|
+
const code = body.code ?? body.error;
|
|
71
|
+
return code === "capability_disabled" && (body.code === undefined || body.code === code) && (body.error === undefined || body.error === code)
|
|
72
|
+
&& body.required_capability === "read_workbench" && (!Object.hasOwn(body, "missing_sections") || Array.isArray(body.missing_sections) && body.missing_sections.length === 0);
|
|
73
|
+
}
|
|
74
|
+
// Derived only from validated protocol metadata; upstream free text is ignored.
|
|
75
|
+
export function calendarStartNotice(value) {
|
|
76
|
+
const body = record(value), rows = body?.missing_sections;
|
|
77
|
+
if (!Array.isArray(rows))
|
|
78
|
+
return "";
|
|
79
|
+
const reasons = new Set(rows.slice(0, 32).filter(v => record(v)?.section === "calendar_start" && record(v)?.required_capability === "read_workbench").map(v => record(v)?.reason));
|
|
80
|
+
if (reasons.size !== 1)
|
|
81
|
+
return "";
|
|
82
|
+
if (reasons.has("record_failed"))
|
|
83
|
+
return "这次没记上开工时间,已读内容仍可使用;不自动重试。";
|
|
84
|
+
if (reasons.has("capability_disabled"))
|
|
85
|
+
return "读取工作台开关未开启,这次未记录开工时间;其余内容按原权限返回。";
|
|
86
|
+
return "";
|
|
87
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { CliError, validToken, networkFailure } from "./errors.js";
|
|
2
|
+
import { isPagedQueryPath, isQueryParameterError, isProjectIDParameterError } from "./query-validation.js";
|
|
3
|
+
import { isSearchParameterError } from "./query-search.js";
|
|
4
|
+
import { isDailyParameterError } from "./query-daily.js";
|
|
5
|
+
import { isCreatePath } from "./create-bundle.js";
|
|
6
|
+
import { isScheduleHeaderPath, scheduleHeaders } from "./schedule-contract.js";
|
|
7
|
+
import { isCalendarHeaderPath, calendarHeaders } from "./calendar-contract.js";
|
|
8
|
+
const maxJSONBytes = 4 * 1024 * 1024;
|
|
9
|
+
const invalidResponse = () => new CliError("invalid_response", "一间草堂返回了无法确认的响应。", "请先核对网页中的实际结果,不要自动重复写入。", 7);
|
|
10
|
+
const invalidRequest = () => new CliError("invalid_request", "请求参数无效。", "请检查命令帮助和输入数据。");
|
|
11
|
+
async function jsonBody(response, queryPath) {
|
|
12
|
+
if (response.status === 204)
|
|
13
|
+
return null;
|
|
14
|
+
if (Number(response.headers.get("content-length")) > maxJSONBytes) {
|
|
15
|
+
await response.body?.cancel();
|
|
16
|
+
throw invalidResponse();
|
|
17
|
+
}
|
|
18
|
+
const reader = response.body?.getReader();
|
|
19
|
+
if (!reader)
|
|
20
|
+
return null;
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let length = 0;
|
|
23
|
+
try {
|
|
24
|
+
for (;;) {
|
|
25
|
+
const { value, done } = await reader.read();
|
|
26
|
+
if (done)
|
|
27
|
+
break;
|
|
28
|
+
length += value.byteLength;
|
|
29
|
+
if (length > maxJSONBytes) {
|
|
30
|
+
await reader.cancel();
|
|
31
|
+
throw invalidResponse();
|
|
32
|
+
}
|
|
33
|
+
chunks.push(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
reader.releaseLock();
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const value = Buffer.concat(chunks).toString("utf8");
|
|
41
|
+
if (queryPath && response.status === 400 && (isQueryParameterError(value) || isProjectIDParameterError(queryPath, value) || (queryPath.endsWith("/m1/search") && isSearchParameterError(value)) || (queryPath.endsWith("/m1/activity/daily") && isDailyParameterError(value))))
|
|
42
|
+
return value;
|
|
43
|
+
if (!value.trim()) {
|
|
44
|
+
if (response.ok)
|
|
45
|
+
throw invalidResponse();
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return JSON.parse(value);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
if (!response.ok)
|
|
52
|
+
return null;
|
|
53
|
+
throw invalidResponse();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function retryAfter(header) {
|
|
57
|
+
if (!header)
|
|
58
|
+
return undefined;
|
|
59
|
+
const seconds = /^\d+$/.test(header) ? Number(header) : (Date.parse(header) - Date.now()) / 1000;
|
|
60
|
+
return Number.isFinite(seconds) && seconds >= 0 ? Math.min(86400, Math.ceil(seconds)) : undefined;
|
|
61
|
+
}
|
|
62
|
+
export class ApiClient {
|
|
63
|
+
baseUrl;
|
|
64
|
+
fetchImpl;
|
|
65
|
+
timeoutMs;
|
|
66
|
+
secrets = new Set();
|
|
67
|
+
signal;
|
|
68
|
+
constructor(baseUrl, options = {}) {
|
|
69
|
+
try {
|
|
70
|
+
const url = new URL(baseUrl);
|
|
71
|
+
if ((url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))) || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
72
|
+
throw invalidRequest();
|
|
73
|
+
this.baseUrl = url.origin;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new CliError("invalid_environment", "接入环境配置无效。", "请使用默认环境或 --env test。");
|
|
77
|
+
}
|
|
78
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
79
|
+
this.timeoutMs = options.timeoutMs ?? 15000;
|
|
80
|
+
this.signal = options.signal;
|
|
81
|
+
}
|
|
82
|
+
outputSecrets() { return [...this.secrets]; }
|
|
83
|
+
async request(path, options = {}) {
|
|
84
|
+
let url;
|
|
85
|
+
try {
|
|
86
|
+
url = new URL(path, this.baseUrl);
|
|
87
|
+
if (!path.startsWith("/api/v1/agent/") || url.origin !== this.baseUrl || url.hash || path.split("?")[0] !== url.pathname)
|
|
88
|
+
throw invalidRequest();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw invalidRequest();
|
|
92
|
+
}
|
|
93
|
+
const method = options.method ?? (options.body === undefined ? "GET" : "POST");
|
|
94
|
+
if (!["GET", "POST", "PATCH", "DELETE"].includes(method) || (method === "GET" && options.body !== undefined))
|
|
95
|
+
throw invalidRequest();
|
|
96
|
+
if (options.token !== undefined && (!validToken(options.token) || /^\/api\/v1\/agent\/device-auth\//.test(url.pathname)))
|
|
97
|
+
throw invalidRequest();
|
|
98
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
99
|
+
if (options.token !== undefined) {
|
|
100
|
+
headers.set("Authorization", `Bearer ${options.token}`);
|
|
101
|
+
this.secrets.add(options.token);
|
|
102
|
+
}
|
|
103
|
+
if (options.body && typeof options.body === "object" && "device_code" in options.body && typeof options.body.device_code === "string")
|
|
104
|
+
this.secrets.add(options.body.device_code);
|
|
105
|
+
if (isScheduleHeaderPath(url.pathname, method)) {
|
|
106
|
+
for (const [name, value] of Object.entries(scheduleHeaders(url.pathname, method, options.headers)))
|
|
107
|
+
headers.set(name, value);
|
|
108
|
+
}
|
|
109
|
+
else if (isCalendarHeaderPath(url.pathname, method)) {
|
|
110
|
+
for (const [name, value] of Object.entries(calendarHeaders(url.pathname, method, options.headers)))
|
|
111
|
+
headers.set(name, value);
|
|
112
|
+
}
|
|
113
|
+
else if (options.headers !== undefined) {
|
|
114
|
+
if (!options.headers || Object.keys(options.headers).some((key) => key !== "Idempotency-Key"))
|
|
115
|
+
throw invalidRequest();
|
|
116
|
+
const key = options.headers["Idempotency-Key"];
|
|
117
|
+
if (key !== undefined) {
|
|
118
|
+
if (typeof key !== "string" || !/^[A-Za-z0-9._:-]{16,128}(?![\s\S])/.test(key) || method !== "POST" || (!/^\/api\/v1\/agent\/m2\/projects\/[^/]+\/updates$/.test(url.pathname) && !isCreatePath(url.pathname)))
|
|
119
|
+
throw invalidRequest();
|
|
120
|
+
headers.set("Idempotency-Key", key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let body;
|
|
124
|
+
if (options.body !== undefined) {
|
|
125
|
+
try {
|
|
126
|
+
body = JSON.stringify(options.body);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw invalidRequest();
|
|
130
|
+
}
|
|
131
|
+
headers.set("Content-Type", "application/json");
|
|
132
|
+
}
|
|
133
|
+
return this.perform(url, { method, headers, body }, this.timeoutMs, options.signal ?? this.signal);
|
|
134
|
+
}
|
|
135
|
+
async upload(value, requiredHeaders, bytes) {
|
|
136
|
+
let url;
|
|
137
|
+
try {
|
|
138
|
+
url = new URL(value);
|
|
139
|
+
if (url.origin !== this.baseUrl || url.username || url.password || url.search || url.hash || !/^\/api\/v1\/knowledge-files\/uploads\/[A-Za-z0-9-]{1,80}$/.test(url.pathname))
|
|
140
|
+
throw invalidRequest();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw invalidRequest();
|
|
144
|
+
}
|
|
145
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < 1 || bytes.byteLength > 10485760 || !requiredHeaders || Object.keys(requiredHeaders).length !== 2)
|
|
146
|
+
throw invalidRequest();
|
|
147
|
+
const headers = new Headers();
|
|
148
|
+
for (const [key, value] of Object.entries(requiredHeaders)) {
|
|
149
|
+
if (!["content-type", "x-opc-upload-token"].includes(key.toLowerCase()) || typeof value !== "string")
|
|
150
|
+
throw invalidRequest();
|
|
151
|
+
headers.set(key, value);
|
|
152
|
+
}
|
|
153
|
+
if (headers.get("Content-Type") !== "application/pdf" || !validToken(headers.get("X-OPC-Upload-Token")))
|
|
154
|
+
throw invalidRequest();
|
|
155
|
+
this.secrets.add(headers.get("X-OPC-Upload-Token"));
|
|
156
|
+
return this.perform(url, { method: "PUT", headers, body: new Uint8Array(bytes).buffer }, 75000, this.signal);
|
|
157
|
+
}
|
|
158
|
+
async perform(url, options, timeoutMs, signal) {
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
const networkError = (error) => networkFailure(error, options.method === "GET" || url.pathname === "/api/v1/agent/mcp-token/validate");
|
|
161
|
+
const cancelled = () => new CliError("cancelled", "操作已取消,结果可能尚未确认。", "请在网页核对,勿直接重复写入。", 130);
|
|
162
|
+
if (signal?.aborted)
|
|
163
|
+
throw cancelled();
|
|
164
|
+
let timer;
|
|
165
|
+
let onAbort;
|
|
166
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
167
|
+
timer = setTimeout(() => { controller.abort(); reject(networkError({ code: "ETIMEDOUT" })); }, timeoutMs);
|
|
168
|
+
onAbort = () => { controller.abort(); reject(cancelled()); };
|
|
169
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
return await Promise.race([(async () => {
|
|
173
|
+
const response = await this.fetchImpl(url, { ...options, cache: "no-store", redirect: "manual", signal: controller.signal });
|
|
174
|
+
let body;
|
|
175
|
+
try {
|
|
176
|
+
body = await jsonBody(response, options.method === "GET" && isPagedQueryPath(url.pathname) ? url.pathname : undefined);
|
|
177
|
+
this.rememberSecrets(body);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (response.ok)
|
|
181
|
+
throw error;
|
|
182
|
+
body = null;
|
|
183
|
+
}
|
|
184
|
+
const retry = retryAfter(response.headers.get("retry-after"));
|
|
185
|
+
return { status: response.status, body, ...(retry === undefined ? {} : { retryAfterSeconds: retry }) };
|
|
186
|
+
})(), deadline]);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (error instanceof CliError)
|
|
190
|
+
throw error;
|
|
191
|
+
throw networkError(error);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
if (onAbort)
|
|
196
|
+
signal?.removeEventListener("abort", onAbort);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
rememberSecrets(value, depth = 0) {
|
|
200
|
+
if (depth > 128)
|
|
201
|
+
throw invalidResponse();
|
|
202
|
+
if (!value || typeof value !== "object")
|
|
203
|
+
return;
|
|
204
|
+
for (const [key, item] of Object.entries(value)) {
|
|
205
|
+
if (/^(token|access_token|token_value|token_hash|device_code|upload_nonce|x-opc-upload-token)$/i.test(key) && typeof item === "string" && item.length >= 8)
|
|
206
|
+
this.secrets.add(item);
|
|
207
|
+
else if (item && typeof item === "object")
|
|
208
|
+
this.rememberSecrets(item, depth + 1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { asRecord, CliError } from "./errors.js";
|
|
2
|
+
import { commandHelp } from "./command-help.js";
|
|
3
|
+
export const trimGoSpace = (value) => value.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "");
|
|
4
|
+
export function inputDetails(command, issues) {
|
|
5
|
+
const help = commandHelp(command);
|
|
6
|
+
return {
|
|
7
|
+
missing_fields: issues.missing ?? [],
|
|
8
|
+
// Even a syntactically harmless key can itself be a token or private text.
|
|
9
|
+
unknown_fields: issues.unknown ? ["<unknown_field>"] : [],
|
|
10
|
+
invalid_fields: issues.invalid ?? [],
|
|
11
|
+
allowed_fields: Object.keys(help?.fields ?? {}),
|
|
12
|
+
requirements: help?.fields ?? {},
|
|
13
|
+
...(help ? { example: help.example } : {}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function invalidInput(command, issues) {
|
|
17
|
+
return new CliError("invalid_arguments", "写入参数无效,请核对列出的缺失字段、未知字段和无效字段。", ["upload", "link", "unlink"].includes(command[0])
|
|
18
|
+
? "请按 missing_fields、unknown_fields、invalid_fields 和 requirements 核对参数,使用 example 中的固定安全命令示例;只提交本人确认的内容,不自动改写或重试。"
|
|
19
|
+
: "请按 missing_fields 补齐字段、删除 unknown_fields 对应的额外字段,并按 requirements 修正 invalid_fields;example 是固定安全示例,不含用户输入。JSON 可用 --input -;只提交本人确认的内容,不自动改写或重试。", 2, inputDetails(command, issues));
|
|
20
|
+
}
|
|
21
|
+
export function validateInput(value, schema, command) {
|
|
22
|
+
const body = asRecord(value);
|
|
23
|
+
if (!body)
|
|
24
|
+
throw invalidInput(command, { invalid: ["$input"] });
|
|
25
|
+
const missing = (schema.required ?? []).filter(key => !Object.hasOwn(body, key));
|
|
26
|
+
const invalid = [];
|
|
27
|
+
let unknown = false;
|
|
28
|
+
const normalized = {};
|
|
29
|
+
for (const [key, original] of Object.entries(body)) {
|
|
30
|
+
if (!Object.hasOwn(schema.fields, key)) {
|
|
31
|
+
unknown = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const item = schema.trimText && typeof original === "string" ? trimGoSpace(original) : original;
|
|
35
|
+
let valid = false;
|
|
36
|
+
try {
|
|
37
|
+
valid = schema.fields[key](item);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (!(error instanceof CliError) || error.code !== "invalid_arguments")
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (!valid)
|
|
44
|
+
invalid.push(key);
|
|
45
|
+
else
|
|
46
|
+
normalized[key] = item;
|
|
47
|
+
}
|
|
48
|
+
if (!Object.keys(body).length && !missing.length)
|
|
49
|
+
invalid.push("$input");
|
|
50
|
+
if (missing.length || unknown || invalid.length)
|
|
51
|
+
throw invalidInput(command, { missing, unknown, invalid });
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
export function flagError(command) {
|
|
55
|
+
const help = commandHelp(command);
|
|
56
|
+
return new CliError("invalid_arguments", "这个命令不支持所给参数,或参数格式无效。", "请只使用 allowed_flags 中列出的参数,并查看该命令 --help;按名称查找请用 caotang search,不支持 projects --q。未知参数名和值不会回显。", 2, {
|
|
57
|
+
...inputDetails(command, { unknown: true }), allowed_flags: [...(help?.options ?? []), "--agent", "--env", "--human", "--help"],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function validateCommandArguments(command, flags) {
|
|
61
|
+
const help = commandHelp(command);
|
|
62
|
+
if (!help)
|
|
63
|
+
return;
|
|
64
|
+
if (Object.entries(flags).some(([key, value]) => value !== undefined && !help.options.includes(`--${key}`)))
|
|
65
|
+
throw flagError(command);
|
|
66
|
+
if (help.command === "context" && flags.start === true && Object.keys(flags).some(key => key !== "start" && flags[key] !== undefined))
|
|
67
|
+
throw flagError(command);
|
|
68
|
+
const count = (help.usage.match(/<[^>]+>/g) ?? []).length;
|
|
69
|
+
const words = help.command.split(" ").length;
|
|
70
|
+
const positionalCount = help.command === "upload" ? 1 : count;
|
|
71
|
+
if (command.length < words + positionalCount) {
|
|
72
|
+
throw invalidInput(command, { missing: (help.usage.match(/<[^>]+>/g) ?? []).slice(command.length - words) });
|
|
73
|
+
}
|
|
74
|
+
if (command.length > words + positionalCount && !(help.command === "search" && command.length === 2))
|
|
75
|
+
throw invalidInput(command, { invalid: ["$arguments"] });
|
|
76
|
+
}
|