@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
@@ -0,0 +1,136 @@
1
+ import { shellWord } from "./agent-identity.js";
2
+ import { constants } from "node:fs";
3
+ import { open } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { authLogin } from "./auth.js";
7
+ import { doctor } from "./doctor.js";
8
+ import { CliError, asRecord } from "./errors.js";
9
+ import { inspectLocalInstallation, installLocal } from "./installation.js";
10
+ import { isPlatformId, platformSupported } from "./local-types.js";
11
+ import { detectPlatforms } from "./platforms.js";
12
+ import { loadSkillAssets } from "./skill-assets.js";
13
+ import { defaultConnectionNames, inspectAccountState, onboardingMessage } from "./onboarding.js";
14
+ import { setupFailureMessage } from "./connection-copy.js";
15
+ const root = () => fileURLToPath(new URL("../", import.meta.url));
16
+ const invalid = () => new CliError("invalid_arguments", "命令或参数无效。", "运行 --help;平台支持claude-code、codex、openclaw,以及仅macOS支持的kimi-code、kimi-work和workbuddy。");
17
+ export function selectedPlatforms(value, operatingSystem = process.platform) {
18
+ if (value === undefined)
19
+ return undefined;
20
+ if (typeof value !== "string")
21
+ throw invalid();
22
+ const values = value.split(",").map(item => item.trim());
23
+ if (!values.length || new Set(values).size !== values.length || values.some(item => !isPlatformId(item)))
24
+ throw invalid();
25
+ if (values.some(id => !platformSupported(id, operatingSystem)))
26
+ throw new CliError("platform_unsupported", "所选新增平台尚不支持当前系统。", "kimi-code、kimi-work和workbuddy仅支持macOS本机安装;不要套用或猜测其它系统的技能目录。");
27
+ return values;
28
+ }
29
+ export async function localVersion(packageRoot = root()) {
30
+ const failed = () => new CliError("version_unavailable", "无法核对当前命令行版本。", "请使用完整的一间草堂命令行构建;不要运行不明升级脚本。", 7);
31
+ try {
32
+ const file = await open(join(packageRoot, "package.json"), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
33
+ try {
34
+ const info = await file.stat();
35
+ if (!info.isFile() || info.nlink !== 1 || info.size < 1 || info.size > 32768)
36
+ throw failed();
37
+ const bytes = Buffer.alloc(32769), { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
38
+ if (bytesRead !== info.size || bytesRead > 32768)
39
+ throw failed();
40
+ const data = asRecord(JSON.parse(bytes.subarray(0, bytesRead).toString("utf8")));
41
+ if (data?.name !== "@yijiancaotang/cli" || (data.private !== true && asRecord(data.publishConfig)?.access !== "public") || typeof data.version !== "string" || !/^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/i.test(data.version))
42
+ throw failed();
43
+ return { status: 200, body: { brand: "一间草堂", name: data.name, version: data.version, node_version: process.versions.node, release_status: "released", update_policy: "manual" } };
44
+ }
45
+ finally {
46
+ await file.close();
47
+ }
48
+ }
49
+ catch {
50
+ throw failed();
51
+ }
52
+ }
53
+ export function manualUpdateInstructions() {
54
+ return { status: 200, body: { operation: "update", update_policy: "manual", next_action: "运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新到最新版。" } };
55
+ }
56
+ async function targetsFor(ctx, ids, setup) {
57
+ if (ctx.agent) {
58
+ if (ids && ids.length !== 1)
59
+ throw new CliError("invalid_arguments", "每次接入只给一个 AI 平台安装技能。", "请带自己的 --agent 名字,一次只选一个 --platform;其它 AI 分别接入。");
60
+ ids = ids ?? (ctx.agent.platform ? [ctx.agent.platform] : []);
61
+ if (!ids.length)
62
+ return [];
63
+ }
64
+ const all = await detectPlatforms({ homeDir: ctx.homeDir, env: ctx.env, applicationRoots: ctx.applicationRoots, operatingSystem: ctx.operatingSystem, platformIds: ids });
65
+ return ids ? all.filter(target => ids.includes(target.id)) : setup ? all.filter(target => target.detected) : all;
66
+ }
67
+ export async function localDoctor(ctx, flags, authFailure) {
68
+ return doctor({
69
+ auth: ctx.auth, authFailure, agent: ctx.agent,
70
+ inspectLocal: async () => {
71
+ const assets = await loadSkillAssets(ctx.packageRoot ?? root());
72
+ return inspectLocalInstallation({ ...ctx, agentName: ctx.agent?.key, assets, targets: await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), false) });
73
+ },
74
+ });
75
+ }
76
+ export async function localSetup(ctx, flags) {
77
+ const assets = await loadSkillAssets(ctx.packageRoot ?? root()), targets = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), true);
78
+ const local = { ...ctx, agentName: ctx.agent?.key, assets, targets };
79
+ const installed = await installLocal(local);
80
+ const newEntries = installed.newlyInstalledPlatforms;
81
+ let authFailure;
82
+ let authorizationAttempted = false;
83
+ // Observe the actual start request after authLogin's credential/abort checks.
84
+ // Validation and doctor requests do not constitute a new authorization attempt.
85
+ const observedAuth = {
86
+ ...ctx.auth,
87
+ api: {
88
+ baseUrl: ctx.auth.api.baseUrl,
89
+ request(path, options) {
90
+ if (path === "/api/v1/agent/device-auth/start" && options?.method === "POST")
91
+ authorizationAttempted = true;
92
+ return ctx.auth.api.request(path, options);
93
+ },
94
+ upload: (url, headers, bytes) => ctx.auth.api.upload(url, headers, bytes),
95
+ outputSecrets: () => ctx.auth.api.outputSecrets?.() ?? [],
96
+ },
97
+ };
98
+ let newAuthorizationOwner;
99
+ let loginIdentity;
100
+ const names = defaultConnectionNames(targets.map(target => target.id), ctx.computerName, ctx.agent?.name);
101
+ try {
102
+ const login = await authLogin(observedAuth, { device_name: flags["device-name"] ?? names.deviceName, client_name: flags["client-name"] ?? names.clientName, noOpen: flags["no-open"] === true });
103
+ const who = asRecord(login.body);
104
+ if (typeof who?.user_id === "string" && typeof who.token_id === "string" && (who.kind === "manual" || who.kind === "device") && (who.scope === "read_only" || who.scope === "read_write")) {
105
+ loginIdentity = { userId: who.user_id, tokenId: who.token_id, kind: who.kind, scope: who.scope };
106
+ }
107
+ if (who?.new_authorization_confirmed === true && typeof who.user_id === "string")
108
+ newAuthorizationOwner = who.user_id;
109
+ }
110
+ catch (error) {
111
+ authFailure = error instanceof CliError ? error : new CliError("authorization_delivery_unknown", "无法确认授权操作的结果。", "先在网页核对并吊销未保存的设备,不要自动重新签发。", 6);
112
+ }
113
+ let checkedIdentity;
114
+ const report = await doctor({ auth: ctx.auth, authFailure, agent: ctx.agent, inspectLocal: () => inspectLocalInstallation(local), onIdentity: who => { checkedIdentity = who; } });
115
+ const body = asRecord(report.result.body);
116
+ if (!body)
117
+ throw new CliError("diagnostic_unavailable", "本地安装已执行,但无法确认最终诊断。", "先运行doctor只读核对,不要直接重新授权。", 7);
118
+ const sameConnection = loginIdentity && checkedIdentity && ["userId", "tokenId", "kind", "scope"].every(key => loginIdentity[key] === checkedIdentity[key]);
119
+ const identityChanged = body.ready === true && !sameConnection;
120
+ if (identityChanged) {
121
+ const issue = { code: "authorization_identity_changed", message: "接入前后的连接核对不一致,尚不能确认接好。", next_action: "请先在网页 Agent 管理和本机 auth status 核对连接;不自动切换账号、重新授权或写入。" };
122
+ body.ready = false;
123
+ body.auth = { status: "unknown" };
124
+ body.issues = [...(Array.isArray(body.issues) ? body.issues : []), issue];
125
+ body.next_actions = [...(Array.isArray(body.next_actions) ? body.next_actions : []), issue.next_action];
126
+ report.exitCode = 6;
127
+ }
128
+ const confirmed = authorizationAttempted && newAuthorizationOwner !== undefined && report.exitCode === 0 && body.ready === true;
129
+ const accountState = newEntries.length && body.ready === true && checkedIdentity ? await inspectAccountState(ctx.auth, checkedIdentity.userId) : "unknown";
130
+ const greeting = body.ready === true && checkedIdentity ? onboardingMessage(newEntries, accountState, checkedIdentity.scope)
131
+ : { account_state: "unknown", onboarding: { action: "none", newly_connected_platforms: [] }, user_message: setupFailureMessage(identityChanged ? "authorization_identity_changed" : authFailure?.code) };
132
+ return { exitCode: report.exitCode, result: { status: 200, body: { ...body, operation: "setup", local_installation: { changed: installed.changed, bundle_sha: installed.bundleSha }, authorization_attempted: authorizationAttempted,
133
+ new_authorization_confirmed: confirmed, ...(confirmed ? { new_authorization_owner_id: newAuthorizationOwner } : {}), ...greeting,
134
+ ...(ctx.agent && !targets.length ? { generic_usage: { message: "这个 AI 没有专用技能安装位置;请按下列命令使用自己的连接。", commands: ["doctor", "context --start", "projects", "knowledge", "search", "auth status"].map(command => `caotang --agent ${shellWord(ctx.agent.name)} ${command}`) } } : {}),
135
+ ...(body.ready === true ? { next_conversation: "请开新对话说‘开工了’。" } : {}) } } };
136
+ }
@@ -0,0 +1,245 @@
1
+ import { constants, openSync, closeSync, lstatSync, fstatSync, readSync, writeSync, fsyncSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { basename, dirname } from "node:path";
4
+ import { asRecord } from "./errors.js";
5
+ const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
6
+ const chunkSize = 65536;
7
+ const recordLimit = 256;
8
+ export function deletionDenied(error) {
9
+ return ["EPERM", "EACCES", "EROFS"].includes(String(asRecord(error)?.code))
10
+ || (error instanceof Error && error.message.includes("[safe-delete] broker denied delete"));
11
+ }
12
+ function dead(pid) {
13
+ try {
14
+ process.kill(pid, 0);
15
+ return false;
16
+ }
17
+ catch (error) {
18
+ return asRecord(error)?.code === "ESRCH";
19
+ }
20
+ }
21
+ // Local regular files only: a single O_APPEND write orders each ticket. The first
22
+ // unreleased live/unknown ticket owns the lock. Never replace or truncate this
23
+ // inode: delayed contenders must see the same history, even after a crash.
24
+ // File count is constant; journal bytes grow with use. Do not mix with the old
25
+ // rename/compaction protocol or put this journal on a network filesystem.
26
+ export function acquireLocalLock(path, parent, failures) {
27
+ const { busy, unsafe } = failures, name = basename(path);
28
+ const ticket = { op: "acquire", pid: process.pid, id: randomUUID() };
29
+ let fd = -1, identity, appended = false;
30
+ const check = (info) => {
31
+ if (!info.isFile() || typeof process.getuid !== "function" || info.uid !== process.getuid()
32
+ || (info.nlink !== 1 && info.nlink !== 0) || (info.mode & 0o7777) !== 0o600
33
+ || !Number.isSafeInteger(info.size) || info.size < 0)
34
+ throw unsafe();
35
+ if (info.nlink === 0)
36
+ throw busy();
37
+ };
38
+ const pinned = (action) => {
39
+ const original = process.cwd();
40
+ try {
41
+ process.chdir(parent.path);
42
+ const info = lstatSync(".");
43
+ if (dirname(path) !== parent.path || !info.isDirectory() || !same(info, parent.identity)
44
+ || info.uid !== parent.identity.uid || (info.mode & 0o7777) !== 0o700)
45
+ throw unsafe();
46
+ return action();
47
+ }
48
+ finally {
49
+ process.chdir(original);
50
+ }
51
+ };
52
+ const current = () => {
53
+ const opened = fstatSync(fd);
54
+ check(opened);
55
+ let named;
56
+ try {
57
+ named = lstatSync(name);
58
+ }
59
+ catch (error) {
60
+ if (asRecord(error)?.code === "ENOENT")
61
+ throw busy();
62
+ throw error;
63
+ }
64
+ check(named);
65
+ if (!same(opened, identity) || !same(named, identity))
66
+ throw busy();
67
+ };
68
+ const decode = (line) => {
69
+ if (!line.length || Buffer.byteLength(line) > recordLimit)
70
+ throw busy();
71
+ let entry;
72
+ try {
73
+ entry = JSON.parse(line);
74
+ }
75
+ catch {
76
+ throw busy();
77
+ }
78
+ if (!entry || Object.keys(entry).sort().join(",") !== "id,op,pid"
79
+ || !["acquire", "release"].includes(entry.op) || !Number.isSafeInteger(entry.pid)
80
+ || entry.pid <= 0 || entry.pid > 2147483647 || typeof entry.id !== "string"
81
+ || !/^[a-f0-9-]{36}$/.test(entry.id))
82
+ throw busy();
83
+ return entry;
84
+ };
85
+ const legacyMarkers = () => {
86
+ for (const legacy of [...Array.from({ length: 16 }, (_, index) => `${name}.admission-${index}`), `${name}.admission-released`, `${name}.next`]) {
87
+ let before;
88
+ try {
89
+ before = lstatSync(legacy);
90
+ }
91
+ catch (error) {
92
+ if (asRecord(error)?.code === "ENOENT")
93
+ continue;
94
+ throw error;
95
+ }
96
+ check(before);
97
+ if (before.size > recordLimit)
98
+ throw unsafe();
99
+ const reader = openSync(legacy, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
100
+ try {
101
+ const opened = fstatSync(reader);
102
+ check(opened);
103
+ if (opened.size > recordLimit)
104
+ throw unsafe();
105
+ if (!same(opened, before))
106
+ throw busy();
107
+ const bytes = Buffer.alloc(recordLimit + 1);
108
+ let count = 0;
109
+ while (count < bytes.length) {
110
+ const read = readSync(reader, bytes, count, bytes.length - count, count);
111
+ if (!read)
112
+ break;
113
+ count += read;
114
+ }
115
+ const after = fstatSync(reader);
116
+ check(after);
117
+ let named;
118
+ try {
119
+ named = lstatSync(legacy);
120
+ }
121
+ catch (error) {
122
+ if (asRecord(error)?.code === "ENOENT")
123
+ throw busy();
124
+ throw error;
125
+ }
126
+ check(named);
127
+ if (count > recordLimit || after.size > recordLimit || named.size > recordLimit)
128
+ throw unsafe();
129
+ if (!same(after, opened) || !same(after, named) || before.size !== after.size
130
+ || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || count !== after.size)
131
+ throw busy();
132
+ if (!count || bytes[count - 1] !== 10)
133
+ throw busy();
134
+ const entry = decode(bytes.subarray(0, count - 1).toString("utf8"));
135
+ if (entry.op !== "release" && !dead(entry.pid))
136
+ throw busy();
137
+ }
138
+ finally {
139
+ closeSync(reader);
140
+ }
141
+ }
142
+ };
143
+ const pending = () => {
144
+ current();
145
+ const before = fstatSync(fd), buffer = Buffer.alloc(chunkSize);
146
+ const active = new Map(), seen = new Set();
147
+ let offset = 0, remainder = "";
148
+ while (offset < before.size) {
149
+ const count = readSync(fd, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
150
+ if (!count)
151
+ throw busy();
152
+ offset += count;
153
+ const lines = (remainder + buffer.subarray(0, count).toString("utf8")).split("\n");
154
+ remainder = lines.pop();
155
+ if (Buffer.byteLength(remainder) > recordLimit)
156
+ throw busy();
157
+ for (const line of lines) {
158
+ const entry = decode(line);
159
+ if (entry.op === "acquire") {
160
+ if (seen.has(entry.id))
161
+ throw busy();
162
+ seen.add(entry.id);
163
+ active.set(entry.id, entry);
164
+ }
165
+ else {
166
+ if (active.get(entry.id)?.pid !== entry.pid)
167
+ throw busy();
168
+ active.delete(entry.id);
169
+ }
170
+ }
171
+ }
172
+ current();
173
+ const after = fstatSync(fd);
174
+ if (remainder || after.size < before.size
175
+ || (after.size === before.size && (after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)))
176
+ throw busy();
177
+ return [...active.values()].filter(entry => !dead(entry.pid));
178
+ };
179
+ const append = (entry) => {
180
+ current();
181
+ const bytes = Buffer.from(JSON.stringify(entry) + "\n");
182
+ if (bytes.length > recordLimit || writeSync(fd, bytes) !== bytes.length)
183
+ throw busy();
184
+ fsyncSync(fd);
185
+ current();
186
+ };
187
+ try {
188
+ pinned(() => {
189
+ let created = false;
190
+ try {
191
+ fd = openSync(name, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_APPEND | constants.O_NOFOLLOW, 0o600);
192
+ created = true;
193
+ }
194
+ catch (error) {
195
+ if (asRecord(error)?.code !== "EEXIST")
196
+ throw error;
197
+ const before = lstatSync(name);
198
+ if (before.isDirectory())
199
+ throw busy();
200
+ check(before);
201
+ fd = openSync(name, constants.O_RDWR | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK);
202
+ check(fstatSync(fd));
203
+ if (!same(before, fstatSync(fd)))
204
+ throw busy();
205
+ }
206
+ identity = fstatSync(fd);
207
+ current();
208
+ legacyMarkers();
209
+ if (created ? identity.size !== 0 : pending().length > 0)
210
+ throw busy();
211
+ append(ticket);
212
+ appended = true;
213
+ if (pending()[0]?.id !== ticket.id)
214
+ throw busy();
215
+ current();
216
+ });
217
+ }
218
+ catch (error) {
219
+ if (appended) {
220
+ try {
221
+ pinned(() => append({ ...ticket, op: "release" }));
222
+ }
223
+ catch { /* Keep uncertain ownership closed. */ }
224
+ }
225
+ if (fd !== -1)
226
+ closeSync(fd);
227
+ throw error;
228
+ }
229
+ let released = false;
230
+ return () => {
231
+ if (released)
232
+ throw busy();
233
+ released = true;
234
+ try {
235
+ pinned(() => {
236
+ if (pending()[0]?.id !== ticket.id)
237
+ throw busy();
238
+ append({ ...ticket, op: "release" });
239
+ });
240
+ }
241
+ finally {
242
+ closeSync(fd);
243
+ }
244
+ };
245
+ }
@@ -0,0 +1,3 @@
1
+ export const PLATFORM_IDS = ["claude-code", "codex", "openclaw", "kimi-code", "kimi-work", "workbuddy"];
2
+ export const isPlatformId = (value) => PLATFORM_IDS.some(id => id === value);
3
+ export const platformSupported = (id, operatingSystem = process.platform) => operatingSystem === "darwin" || (id !== "kimi-code" && id !== "kimi-work" && id !== "workbuddy");
@@ -0,0 +1,71 @@
1
+ import { asRecord } from "./errors.js";
2
+ const knownSections = {
3
+ daily_progress: { capability: "read_projects", label: "日汇总中的进展" },
4
+ daily_knowledge: { capability: "read_knowledge", label: "日汇总中的知识新增与长势" },
5
+ search_projects: { capability: "read_projects", label: "搜索中的项目与进展" },
6
+ search_knowledge: { capability: "read_knowledge", label: "搜索中的知识" },
7
+ workbench: { capability: "read_workbench", label: "工作台" },
8
+ briefing: { capability: "read_notifications", label: "简报" },
9
+ knowledge: { capability: "read_knowledge", label: "知识内容" },
10
+ recent_knowledge: { capability: "read_knowledge", label: "最近知识" },
11
+ draft_knowledge: { capability: "read_drafts", label: "草稿知识" },
12
+ recent_projects: { capability: "read_projects", label: "最近项目" },
13
+ project_updates: { capability: "read_projects", label: "项目进展" },
14
+ draft_projects: { capability: "read_projects", label: "项目草稿" },
15
+ project_drafts: { capability: "read_drafts", label: "项目草稿" },
16
+ project_draft_checkins: { capability: "read_drafts", label: "项目草稿关联的手写进展" },
17
+ project_evidence: { capability: "read_projects", label: "项目证据文字" },
18
+ draft_project_evidence: { capability: "read_drafts", label: "项目草稿证据文字" },
19
+ knowledge_applied_updates: { capability: "read_knowledge", label: "知识应用进展正文" },
20
+ draft_knowledge_applied_updates: { capability: "read_drafts", label: "可能含草稿知识的应用进展正文" },
21
+ knowledge_result: { capability: "read_knowledge", label: "知识写入结果" },
22
+ project_result: { capability: "read_projects", label: "项目写入结果" },
23
+ draft_knowledge_result: { capability: "read_drafts", label: "知识草稿写入结果" },
24
+ draft_project_result: { capability: "read_drafts", label: "项目草稿写入结果" },
25
+ };
26
+ const capabilityLabels = {
27
+ read_workbench: "读取工作台", read_notifications: "读取通知", read_knowledge: "读取知识",
28
+ read_drafts: "读取草稿", read_projects: "读取项目",
29
+ };
30
+ function reasonLabel(reason) {
31
+ if (reason === "capability_disabled")
32
+ return "开关未开启";
33
+ if (reason === "token_scope_insufficient")
34
+ return "设备权限范围不足";
35
+ if (["permission_denied", "forbidden", "access_denied", "authorization_unavailable", "moderation_blocked"].includes(reason))
36
+ return "权限不足或访问受限";
37
+ return "具体原因尚未核实";
38
+ }
39
+ /** Read only protocol locations; never traverse cards, titles, summaries or payloads. */
40
+ export function formatMissingSections(value) {
41
+ const body = asRecord(value);
42
+ if (!body)
43
+ return "";
44
+ const sources = [
45
+ { prefix: "", value: body },
46
+ { prefix: "工作台中的", value: asRecord(body.workbench) },
47
+ { prefix: "简报中的", value: asRecord(body.briefing) },
48
+ ];
49
+ const lines = [], seen = new Set();
50
+ for (const source of sources) {
51
+ const rows = source.value?.missing_sections;
52
+ if (!Array.isArray(rows))
53
+ continue;
54
+ // Bound invalid/repeated server metadata to the fixed protocol section set.
55
+ for (const row of rows.slice(0, 64)) {
56
+ const item = asRecord(row);
57
+ if (!item || typeof item.section !== "string" || !Object.hasOwn(knownSections, item.section)
58
+ || typeof item.reason !== "string")
59
+ continue;
60
+ const section = knownSections[item.section];
61
+ if (item.required_capability !== section.capability)
62
+ continue;
63
+ const key = source.prefix + item.section;
64
+ if (seen.has(key))
65
+ continue;
66
+ seen.add(key);
67
+ lines.push(`- ${source.prefix}${section.label}:未读取,${reasonLabel(item.reason)}。请在网页检查“${capabilityLabels[section.capability]}”开关及设备权限。`);
68
+ }
69
+ }
70
+ return lines.length ? `部分内容未读取,这不代表没有资料:\n${lines.join("\n")}\n` : "";
71
+ }
@@ -0,0 +1,48 @@
1
+ import { hostname } from "node:os";
2
+ import { resolveCredential } from "./auth.js";
3
+ import { verifiedHabit } from "./habit-wire.js";
4
+ import { connectionCopy } from "./connection-copy.js";
5
+ export const platformNames = {
6
+ "claude-code": "Claude Code", codex: "Codex", openclaw: "OpenClaw",
7
+ "kimi-code": "Kimi Code", "kimi-work": "Kimi 工作模式", workbuddy: "WorkBuddy",
8
+ };
9
+ export function defaultConnectionNames(platforms, computerName = hostname(), agentName) {
10
+ if (agentName !== undefined)
11
+ return { clientName: agentName, deviceName: agentName };
12
+ const clientName = agentName ?? (platforms.length === 1 ? platformNames[platforms[0]] : "caotang");
13
+ const name = computerName.trim().replace(/\.local$/i, "");
14
+ // Unknown/random/container hosts are not reliable names for a person's computer.
15
+ const reliable = [...name].length <= 48 && /[a-z\p{Script=Han}]/iu.test(name)
16
+ && !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\/\\@]/u.test(name)
17
+ && !/^(?:[a-f\d-]{12,}|ip-\d|localhost$|runner|docker|container|sandbox|codespace|pod-)/i.test(name);
18
+ return { clientName, deviceName: reliable ? `${clientName} · ${name}` : clientName };
19
+ }
20
+ export async function inspectAccountState(auth, owner) {
21
+ if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(owner))
22
+ return "unknown";
23
+ try {
24
+ const credential = await resolveCredential(auth);
25
+ const result = await auth.api.request("/api/v1/agent/m1/habit/start", { method: "GET", token: credential.token, signal: auth.signal });
26
+ if (result.status !== 200)
27
+ return "unknown";
28
+ const body = verifiedHabit(result.body, true);
29
+ if (body?.owner_id !== owner)
30
+ return "unknown";
31
+ return body.account_empty === true ? "empty" : body.account_empty === false ? "has_content" : "unknown";
32
+ }
33
+ catch {
34
+ return "unknown";
35
+ }
36
+ }
37
+ export function onboardingMessage(newlyConnected, state, scope) {
38
+ const action = newlyConnected.length === 0 ? "none" : state === "empty" ? "introduce" : "ask_once";
39
+ const message = action === "none" ? connectionCopy.repeated
40
+ : scope === "read_only" ? action === "ask_once" ? connectionCopy.read_only_existing : connectionCopy.read_only
41
+ : action === "introduce" ? connectionCopy.first_empty : connectionCopy.first_existing;
42
+ return {
43
+ account_state: state,
44
+ onboarding: { action, newly_connected_platforms: newlyConnected },
45
+ user_message: message,
46
+ skill_activation: newlyConnected.map(platform => ({ platform, message: platform === "codex" || platform === "claude-code" ? "新对话可以加载本次安装的技能。" : "如果看不到技能,请开新对话或重启应用;本次没有自动重启。" })),
47
+ };
48
+ }
package/dist/output.js ADDED
@@ -0,0 +1,52 @@
1
+ import { asRecord, CliError } from "./errors.js";
2
+ import { formatMissingSections } from "./missing-sections.js";
3
+ import { projectedWriteCompleted } from "./write-permissions.js";
4
+ import { formatSearchWarnings } from "./query-search.js";
5
+ import { formatDailyNotice } from "./query-daily.js";
6
+ import { createDraftNotice } from "./create-bundle.js";
7
+ const sensitiveKey = /^(token|access_token|token_value|token_hash|device_code|upload_nonce|upload_url|required_headers|authorization|cookie|set-cookie|x-opc-upload-token)$/i;
8
+ export function redact(value, secrets, depth = 0) {
9
+ if (depth > 128)
10
+ throw new CliError("invalid_response", "响应层级超出可安全输出范围。", "请缩小读取范围后核对。", 7);
11
+ if (typeof value === "string") {
12
+ let safe = value;
13
+ for (const secret of secrets)
14
+ if (secret.length >= 8)
15
+ safe = safe.split(secret).join("[REDACTED]");
16
+ return safe;
17
+ }
18
+ if (Array.isArray(value))
19
+ return value.map((item) => redact(item, secrets, depth + 1));
20
+ if (value !== null && typeof value === "object")
21
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !sensitiveKey.test(key) && !secrets.some((secret) => secret.length >= 8 && key.includes(secret))).map(([key, item]) => [key, redact(item, secrets, depth + 1)]));
22
+ return value;
23
+ }
24
+ export function renderResult(result, human, secrets) {
25
+ const data = redact({ http_status: result.status, data: result.body, ...(result.operationAudit?.length ? { operation_audit: result.operationAudit } : {}) }, secrets);
26
+ const missing = human ? formatMissingSections(asRecord(data)?.data) : "";
27
+ const writeNotice = human && projectedWriteCompleted(result) ? "写入已完成;以下内容因读取权限未开启而没有返回,不是写入失败。\n" : "";
28
+ const body = asRecord(result.body);
29
+ const unknownWrite = explicitUnknownWrite(result);
30
+ const pendingNotice = human && result.status === 202
31
+ ? body?.original_receipt === true && body.idempotency_replayed === true ? "这是原回执,当前状态请到网页核对;本次未重新执行。\n" : unknownWrite ? "写入结果暂无法确认,请先在网页核对;不要自动重试。\n" : "请求仍待处理,请在网页核对。\n" : "";
32
+ const warningNotice = human ? formatSearchWarnings(asRecord(data)?.data) : "";
33
+ const dailyNotice = human ? formatDailyNotice(asRecord(data)?.data) : "";
34
+ const draftNotice = human && [200, 201].includes(result.status) ? createDraftNotice(asRecord(data)?.data) : "";
35
+ const replayNotice = human && [200, 201].includes(result.status) && body?.idempotency_replayed === true ? "这是已有创建请求的回放结果,本次没有重新创建。\n" : "";
36
+ return `${pendingNotice}${replayNotice}${writeNotice}${missing}${warningNotice}${dailyNotice}${draftNotice}${JSON.stringify(data, null, human ? 2 : undefined)}\n`;
37
+ }
38
+ // Only coherent transport/receipt evidence classifies the submitted write;
39
+ // neither a drafted audit log nor a moderation-pending 202 means unknown.
40
+ export function explicitUnknownWrite(result) {
41
+ const b = asRecord(result.body);
42
+ if (result.status !== 202 || !b)
43
+ return false;
44
+ return (b.code === undefined || b.code === "write_outcome_unknown")
45
+ && (b.error === undefined || b.error === "write_outcome_unknown")
46
+ && (b.write_status === undefined || b.write_status === "unknown")
47
+ && (b.code === "write_outcome_unknown" || b.error === "write_outcome_unknown" || b.write_status === "unknown");
48
+ }
49
+ export function renderError(error, human, secrets) {
50
+ const data = redact(error.toJSON(), secrets);
51
+ return `${human ? formatMissingSections(data) : ""}${JSON.stringify(data, null, human ? 2 : undefined)}\n`;
52
+ }
@@ -0,0 +1,45 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { asRecord, CliError } from "./errors.js";
3
+ import { expectCapabilityResult } from "./capability-result.js";
4
+ import { validateInput } from "./input-validation.js";
5
+ const uuid = (value) => typeof value === "string" && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
6
+ const invalid = () => new CliError("invalid_arguments", "待确认申请参数无效。", "使用 --input 提供目标类型knowledge_card或project、目标UUID和reason;改可见性另填public或private,不提供确认、权限或幂等参数。");
7
+ /** Mirrors the approved MCP pending envelope without importing MCP runtime dependencies. */
8
+ export function createPendingRequest(operation, value) {
9
+ if (!["delete", "archive", "visibility"].includes(operation))
10
+ throw invalid();
11
+ const input = validateInput(value, {
12
+ fields: {
13
+ target_type: value => typeof value === "string" && ["knowledge_card", "project"].includes(value),
14
+ target_id: uuid,
15
+ reason: value => typeof value === "string" && value.trim().length > 0 && [...value.trim()].length <= 1000,
16
+ expires_in_hours: value => Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 168,
17
+ ...(operation === "visibility" ? { visibility: (value) => typeof value === "string" && ["public", "private"].includes(value) } : {}),
18
+ },
19
+ required: ["target_type", "target_id", "reason", ...(operation === "visibility" ? ["visibility"] : [])],
20
+ }, ["request", operation]);
21
+ const visibility = operation === "visibility";
22
+ const label = input.target_type === "project" ? "项目" : "知识卡";
23
+ const shortID = input.target_id.slice(0, 8);
24
+ const summary = visibility ? `待确认可见性变更:${label} (${shortID}) -> ${input.visibility}` : `待确认${operation === "archive" ? "归档" : "删除"}:${label} (${shortID})`;
25
+ return {
26
+ capability: "delete_or_archive",
27
+ tool_name: visibility ? "request_public_visibility_change" : "request_delete_or_archive",
28
+ action_type: visibility ? "public_visibility_change" : "delete_or_archive",
29
+ source_type: "manual", source_id: input.target_id, summary,
30
+ payload_json: { target_type: input.target_type, target_id: input.target_id, ...(visibility ? { visibility: input.visibility } : { operation }), reason: input.reason },
31
+ ...(input.expires_in_hours === undefined ? {} : { expires_in_hours: input.expires_in_hours }),
32
+ };
33
+ }
34
+ export async function requestPendingAction(operation, input, ctx) {
35
+ const request = createPendingRequest(operation, input);
36
+ const result = expectCapabilityResult(await ctx.api.request("/api/v1/agent/pending-actions", { method: "POST", token: ctx.token, body: request }), "delete_or_archive", "write");
37
+ const body = asRecord(result.body), pending = asRecord(body?.pending_action), log = asRecord(body?.action_log);
38
+ if (result.status !== 202 || body?.requires_confirmation !== true || !uuid(body.pending_action_id)
39
+ || pending?.id !== body.pending_action_id || pending.status !== "pending" || pending.action_type !== request.action_type
40
+ || !isDeepStrictEqual(pending.payload_json, request.payload_json)
41
+ || log?.action_status !== "drafted" || log.tool_name !== request.tool_name) {
42
+ throw new CliError("pending_result_unknown", "无法核实待确认申请的创建结果。", "先到网页核对待确认记录;不要自动重试或重复发起申请。", 6, { http_status: result.status });
43
+ }
44
+ return { ...result, body: { ...body, message: "申请已创建,等你在网页确认。" } };
45
+ }