@yijiancaotang/cli 0.0.0-stage → 1.0.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.
Files changed (48) hide show
  1. package/README.md +279 -2
  2. package/bin/caotang.cjs +15 -0
  3. package/dist/agent-identity.js +14 -0
  4. package/dist/auth.js +337 -0
  5. package/dist/capability-result.js +80 -0
  6. package/dist/cli.js +248 -0
  7. package/dist/command-help.js +100 -0
  8. package/dist/commands.js +350 -0
  9. package/dist/config.js +14 -0
  10. package/dist/connection-copy.js +40 -0
  11. package/dist/context.js +30 -0
  12. package/dist/contracts.js +1 -0
  13. package/dist/create-bundle.js +103 -0
  14. package/dist/create-file-failure.js +18 -0
  15. package/dist/credential-store.js +594 -0
  16. package/dist/doctor.js +214 -0
  17. package/dist/errors.js +214 -0
  18. package/dist/generated/knowledge-growth.js +37 -0
  19. package/dist/habit-day.js +58 -0
  20. package/dist/habit-read.js +30 -0
  21. package/dist/habit-wire.js +56 -0
  22. package/dist/http.js +201 -0
  23. package/dist/input-validation.js +76 -0
  24. package/dist/installation.js +491 -0
  25. package/dist/knowledge-growth.js +85 -0
  26. package/dist/local-commands.js +136 -0
  27. package/dist/local-lock.js +245 -0
  28. package/dist/local-types.js +3 -0
  29. package/dist/missing-sections.js +71 -0
  30. package/dist/onboarding.js +48 -0
  31. package/dist/output.js +52 -0
  32. package/dist/pending-requests.js +45 -0
  33. package/dist/platforms.js +200 -0
  34. package/dist/query-daily.js +70 -0
  35. package/dist/query-search.js +42 -0
  36. package/dist/query-validation.js +45 -0
  37. package/dist/skill-assets.js +136 -0
  38. package/dist/upload.js +130 -0
  39. package/dist/web-citations.js +68 -0
  40. package/dist/write-permissions.js +41 -0
  41. package/package.json +27 -4
  42. package/skills/caotang-context/SKILL.md +47 -0
  43. package/skills/caotang-first-connect/SKILL.md +62 -0
  44. package/skills/caotang-project-progress/SKILL.md +87 -0
  45. package/skills/caotang-save-knowledge/SKILL.md +40 -0
  46. package/skills/caotang-search/SKILL.md +68 -0
  47. package/skills/caotang-workday/SKILL.md +34 -0
  48. package/skills/manifest.json +11 -0
package/dist/http.js ADDED
@@ -0,0 +1,201 @@
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
+ const maxJSONBytes = 4 * 1024 * 1024;
7
+ const invalidResponse = () => new CliError("invalid_response", "一间草堂返回了无法确认的响应。", "请先核对网页中的实际结果,不要自动重复写入。", 7);
8
+ const invalidRequest = () => new CliError("invalid_request", "请求参数无效。", "请检查命令帮助和输入数据。");
9
+ async function jsonBody(response, queryPath) {
10
+ if (response.status === 204)
11
+ return null;
12
+ if (Number(response.headers.get("content-length")) > maxJSONBytes) {
13
+ await response.body?.cancel();
14
+ throw invalidResponse();
15
+ }
16
+ const reader = response.body?.getReader();
17
+ if (!reader)
18
+ return null;
19
+ const chunks = [];
20
+ let length = 0;
21
+ try {
22
+ for (;;) {
23
+ const { value, done } = await reader.read();
24
+ if (done)
25
+ break;
26
+ length += value.byteLength;
27
+ if (length > maxJSONBytes) {
28
+ await reader.cancel();
29
+ throw invalidResponse();
30
+ }
31
+ chunks.push(value);
32
+ }
33
+ }
34
+ finally {
35
+ reader.releaseLock();
36
+ }
37
+ try {
38
+ const value = Buffer.concat(chunks).toString("utf8");
39
+ if (queryPath && response.status === 400 && (isQueryParameterError(value) || isProjectIDParameterError(queryPath, value) || (queryPath.endsWith("/m1/search") && isSearchParameterError(value)) || (queryPath.endsWith("/m1/activity/daily") && isDailyParameterError(value))))
40
+ return value;
41
+ if (!value.trim()) {
42
+ if (response.ok)
43
+ throw invalidResponse();
44
+ return null;
45
+ }
46
+ return JSON.parse(value);
47
+ }
48
+ catch {
49
+ if (!response.ok)
50
+ return null;
51
+ throw invalidResponse();
52
+ }
53
+ }
54
+ function retryAfter(header) {
55
+ if (!header)
56
+ return undefined;
57
+ const seconds = /^\d+$/.test(header) ? Number(header) : (Date.parse(header) - Date.now()) / 1000;
58
+ return Number.isFinite(seconds) && seconds >= 0 ? Math.min(86400, Math.ceil(seconds)) : undefined;
59
+ }
60
+ export class ApiClient {
61
+ baseUrl;
62
+ fetchImpl;
63
+ timeoutMs;
64
+ secrets = new Set();
65
+ signal;
66
+ constructor(baseUrl, options = {}) {
67
+ try {
68
+ const url = new URL(baseUrl);
69
+ 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 !== "/")
70
+ throw invalidRequest();
71
+ this.baseUrl = url.origin;
72
+ }
73
+ catch {
74
+ throw new CliError("invalid_environment", "接入环境配置无效。", "请使用默认环境或 --env test。");
75
+ }
76
+ this.fetchImpl = options.fetchImpl ?? fetch;
77
+ this.timeoutMs = options.timeoutMs ?? 15000;
78
+ this.signal = options.signal;
79
+ }
80
+ outputSecrets() { return [...this.secrets]; }
81
+ async request(path, options = {}) {
82
+ let url;
83
+ try {
84
+ url = new URL(path, this.baseUrl);
85
+ if (!path.startsWith("/api/v1/agent/") || url.origin !== this.baseUrl || url.hash || path.split("?")[0] !== url.pathname)
86
+ throw invalidRequest();
87
+ }
88
+ catch {
89
+ throw invalidRequest();
90
+ }
91
+ const method = options.method ?? (options.body === undefined ? "GET" : "POST");
92
+ if (!["GET", "POST", "PATCH", "DELETE"].includes(method) || (method === "GET" && options.body !== undefined))
93
+ throw invalidRequest();
94
+ if (options.token !== undefined && (!validToken(options.token) || /^\/api\/v1\/agent\/device-auth\//.test(url.pathname)))
95
+ throw invalidRequest();
96
+ const headers = new Headers({ Accept: "application/json" });
97
+ if (options.token !== undefined) {
98
+ headers.set("Authorization", `Bearer ${options.token}`);
99
+ this.secrets.add(options.token);
100
+ }
101
+ if (options.body && typeof options.body === "object" && "device_code" in options.body && typeof options.body.device_code === "string")
102
+ this.secrets.add(options.body.device_code);
103
+ if (options.headers !== undefined) {
104
+ if (!options.headers || Object.keys(options.headers).some((key) => key !== "Idempotency-Key"))
105
+ throw invalidRequest();
106
+ const key = options.headers["Idempotency-Key"];
107
+ if (key !== undefined) {
108
+ 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)))
109
+ throw invalidRequest();
110
+ headers.set("Idempotency-Key", key);
111
+ }
112
+ }
113
+ let body;
114
+ if (options.body !== undefined) {
115
+ try {
116
+ body = JSON.stringify(options.body);
117
+ }
118
+ catch {
119
+ throw invalidRequest();
120
+ }
121
+ headers.set("Content-Type", "application/json");
122
+ }
123
+ return this.perform(url, { method, headers, body }, this.timeoutMs, options.signal ?? this.signal);
124
+ }
125
+ async upload(value, requiredHeaders, bytes) {
126
+ let url;
127
+ try {
128
+ url = new URL(value);
129
+ 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))
130
+ throw invalidRequest();
131
+ }
132
+ catch {
133
+ throw invalidRequest();
134
+ }
135
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength < 1 || bytes.byteLength > 10485760 || !requiredHeaders || Object.keys(requiredHeaders).length !== 2)
136
+ throw invalidRequest();
137
+ const headers = new Headers();
138
+ for (const [key, value] of Object.entries(requiredHeaders)) {
139
+ if (!["content-type", "x-opc-upload-token"].includes(key.toLowerCase()) || typeof value !== "string")
140
+ throw invalidRequest();
141
+ headers.set(key, value);
142
+ }
143
+ if (headers.get("Content-Type") !== "application/pdf" || !validToken(headers.get("X-OPC-Upload-Token")))
144
+ throw invalidRequest();
145
+ this.secrets.add(headers.get("X-OPC-Upload-Token"));
146
+ return this.perform(url, { method: "PUT", headers, body: new Uint8Array(bytes).buffer }, 75000, this.signal);
147
+ }
148
+ async perform(url, options, timeoutMs, signal) {
149
+ const controller = new AbortController();
150
+ const networkError = (error) => networkFailure(error, options.method === "GET" || url.pathname === "/api/v1/agent/mcp-token/validate");
151
+ const cancelled = () => new CliError("cancelled", "操作已取消,结果可能尚未确认。", "请在网页核对,勿直接重复写入。", 130);
152
+ if (signal?.aborted)
153
+ throw cancelled();
154
+ let timer;
155
+ let onAbort;
156
+ const deadline = new Promise((_resolve, reject) => {
157
+ timer = setTimeout(() => { controller.abort(); reject(networkError({ code: "ETIMEDOUT" })); }, timeoutMs);
158
+ onAbort = () => { controller.abort(); reject(cancelled()); };
159
+ signal?.addEventListener("abort", onAbort, { once: true });
160
+ });
161
+ try {
162
+ return await Promise.race([(async () => {
163
+ const response = await this.fetchImpl(url, { ...options, cache: "no-store", redirect: "manual", signal: controller.signal });
164
+ let body;
165
+ try {
166
+ body = await jsonBody(response, options.method === "GET" && isPagedQueryPath(url.pathname) ? url.pathname : undefined);
167
+ this.rememberSecrets(body);
168
+ }
169
+ catch (error) {
170
+ if (response.ok)
171
+ throw error;
172
+ body = null;
173
+ }
174
+ const retry = retryAfter(response.headers.get("retry-after"));
175
+ return { status: response.status, body, ...(retry === undefined ? {} : { retryAfterSeconds: retry }) };
176
+ })(), deadline]);
177
+ }
178
+ catch (error) {
179
+ if (error instanceof CliError)
180
+ throw error;
181
+ throw networkError(error);
182
+ }
183
+ finally {
184
+ clearTimeout(timer);
185
+ if (onAbort)
186
+ signal?.removeEventListener("abort", onAbort);
187
+ }
188
+ }
189
+ rememberSecrets(value, depth = 0) {
190
+ if (depth > 128)
191
+ throw invalidResponse();
192
+ if (!value || typeof value !== "object")
193
+ return;
194
+ for (const [key, item] of Object.entries(value)) {
195
+ 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)
196
+ this.secrets.add(item);
197
+ else if (item && typeof item === "object")
198
+ this.rememberSecrets(item, depth + 1);
199
+ }
200
+ }
201
+ }
@@ -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
+ }