@yijiancaotang/cli 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -26
- package/dist/auth.js +7 -4
- 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 +5 -0
- package/dist/cli.js +58 -10
- package/dist/command-help.js +18 -8
- package/dist/commands.js +11 -0
- package/dist/doctor.js +13 -5
- package/dist/generated/agent-operations.js +12 -0
- package/dist/habit-read.js +3 -1
- package/dist/habit-wire.js +36 -5
- package/dist/handoff-read.js +29 -0
- package/dist/handoff-wire.js +42 -0
- package/dist/http.js +11 -1
- package/dist/input-validation.js +14 -0
- package/dist/installation.js +334 -45
- package/dist/local-commands.js +133 -16
- package/dist/local-records.js +150 -0
- package/dist/local-types.js +1 -0
- package/dist/managed-skill-baseline.js +35 -0
- package/dist/missing-sections.js +1 -0
- package/dist/onboarding.js +4 -20
- package/dist/output.js +7 -1
- package/dist/platforms.js +69 -12
- package/dist/project-binding-cli.js +110 -0
- package/dist/project-binding.js +370 -0
- package/dist/query-search.js +3 -2
- 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/setup-guidance.js +40 -0
- package/dist/skill-assets.js +1 -1
- package/dist/skills-export.js +206 -0
- package/dist/web-citations.js +1 -1
- package/dist/work-context-human.js +67 -0
- package/dist/work-context-wire.js +391 -0
- package/dist/write-routing.js +26 -0
- package/package.json +2 -2
- package/skills/caotang-context/SKILL.md +20 -7
- package/skills/caotang-first-connect/SKILL.md +48 -34
- package/skills/caotang-project-progress/SKILL.md +6 -2
- package/skills/caotang-save-knowledge/SKILL.md +6 -4
- package/skills/caotang-schedule/SKILL.md +54 -0
- package/skills/caotang-search/SKILL.md +11 -0
- package/skills/caotang-workday/SKILL.md +56 -9
- package/skills/manifest.json +28 -6
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const id = "11111111-1111-4111-8111-111111111111";
|
|
2
|
+
const key = "schedule-example-key-0001";
|
|
3
|
+
const notes = "先复述标题、完整日期和时间(含时区);没日期明确说“先不定日期”,本人确认后才传 confirmed:true。服务端不能验证口头确认。所有写必须用16-128位安全ASCII幂等键;未知202不是成功,保留原键和连接,用 schedule status 核对,不自动重试、换键或执行依赖写入。读取归 read_workbench,写入归 write_schedule,只读设备不能写;项目标签受 read_projects/read_drafts 限制。没有删除接口。";
|
|
4
|
+
const fields = {
|
|
5
|
+
title: "create必填,update选填;按Go规则去首尾空白后1-200个Unicode码点,不含控制符。",
|
|
6
|
+
due_date: "选填,真实YYYY-MM-DD;null或空字符串清除时间,省略保持(create默认不定日期)。",
|
|
7
|
+
due_at: "选填,RFC3339含Z或明确时区偏移,例如2026-11-09T12:00:00+08:00;可null清除。due_date与due_at不能同时非空。",
|
|
8
|
+
project_id: "选填,本人项目UUID;null或空字符串解除关联,省略保持。",
|
|
9
|
+
confirmed: "必填且只能为true;表示调用方已取得本人确认,不代表服务器验证了口头确认。",
|
|
10
|
+
};
|
|
11
|
+
export const scheduleCommandHelp = {
|
|
12
|
+
"schedule list": {
|
|
13
|
+
command: "schedule list", usage: "caotang schedule list", description: "读取本人日程/待办,保留服务端排序、分组及分页;取消和完成记录永久保留。",
|
|
14
|
+
options: ["--view", "--status", "--group-by", "--project", "--q", "--start-date", "--end-date", "--limit", "--offset"],
|
|
15
|
+
fields: { view: "pending(默认)、workbench、recent_completed或history。", status: "仅view=history可传:all、completed、cancelled。", "group-by": "date(默认)或project。", project: "none表示不属于项目,否则传项目UUID;省略为全部。", "start-date": "北京时间含首尾日期YYYY-MM-DD;只到日期或精确时间,未定日期不进入日期范围。", "end-date": "YYYY-MM-DD,不早于start-date。", limit: "1-100;默认20,workbench默认5。", offset: "0至9007199254740991。", q: "标题关键字,去首尾空白后最多200个Unicode码点。" },
|
|
16
|
+
example: { command: "caotang schedule list --view history --status cancelled --limit 100" }, notes,
|
|
17
|
+
},
|
|
18
|
+
"schedule show": { command: "schedule show", usage: "caotang schedule show <id>", description: "读取一条日程及schedule_item.revision;写入时使用这一版本,不猜版本号。", options: [], example: { command: `caotang schedule show ${id}` }, notes },
|
|
19
|
+
"schedule status": { command: "schedule status", usage: "caotang schedule status --idempotency-key KEY", description: "只读核对同一连接的原写入回执;原键仅发送为Idempotency-Key header,不在query/body。未知结果退出非0。", options: ["--idempotency-key"], example: { command: `caotang schedule status --idempotency-key ${key}` }, notes },
|
|
20
|
+
};
|
|
21
|
+
for (const operation of ["create", "update", "complete", "cancel", "undo", "add", "done"]) {
|
|
22
|
+
const create = operation === "create" || operation === "add";
|
|
23
|
+
const edit = create || operation === "update";
|
|
24
|
+
const command = `schedule ${operation}`;
|
|
25
|
+
const suffix = `${create ? "" : " <id>"} --input file|- --idempotency-key KEY${create ? "" : " --revision N"}`;
|
|
26
|
+
scheduleCommandHelp[command] = {
|
|
27
|
+
command, usage: `caotang ${command}${suffix}`,
|
|
28
|
+
description: operation === "add" ? "create兼容别名,确认后新增待办。" : operation === "done" ? "complete兼容别名,确认后标完成。" : operation === "undo" ? "仅撤销刚完成的记录,约5秒窗口;不恢复取消、不删除记录。" : operation === "update" ? "按presence修改待办,只提交确认过的字段,至少一个业务字段。" : `确认后${{ create: "新增待办", complete: "标完成", cancel: "取消待办,保留记录" }[operation]}。`,
|
|
29
|
+
options: ["--input", "--idempotency-key", ...(create ? [] : ["--revision"])],
|
|
30
|
+
fields: { ...(edit ? fields : { confirmed: fields.confirmed }), ...(!create ? { revision: "必填--revision N,来自最近读回的正安全整数revision;发送strong If-Match:双引号包围N。冲突后核对,不自动重发。" } : {}) },
|
|
31
|
+
required_fields: [...(create ? ["title"] : []), "confirmed"],
|
|
32
|
+
example: { command: `caotang ${command}${create ? "" : ` ${id}`} --input - --idempotency-key ${key}${create ? "" : " --revision 3"}`, input: create ? { title: "本人确认的待办", confirmed: true } : operation === "update" ? { due_date: "2026-11-09", confirmed: true } : { confirmed: true } }, notes,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export const scheduleCommandUsages = Object.values(scheduleCommandHelp).map(item => item.usage.replace(/^caotang /, ""));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { validDate as validDay } from "./query-validation.js";
|
|
2
|
+
const row = (value) => {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4
|
+
throw Error();
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
const count = (value) => {
|
|
8
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
9
|
+
throw Error();
|
|
10
|
+
return value;
|
|
11
|
+
};
|
|
12
|
+
const text = (value, max) => {
|
|
13
|
+
if (typeof value !== "string" || !value || [...value].length > max || /[\p{Cc}\u2028\u2029\uD800-\uDFFF]/u.test(value))
|
|
14
|
+
throw Error();
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const identity = (value) => {
|
|
18
|
+
const result = text(value, 36);
|
|
19
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(result))
|
|
20
|
+
throw Error();
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
const projectTitle = (value) => {
|
|
24
|
+
if (typeof value !== "string" || !value || [...value].length > 80 || /[\u0000\uD800-\uDFFF]/u.test(value))
|
|
25
|
+
throw Error();
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
const timestamp = (value) => {
|
|
29
|
+
const result = text(value, 40);
|
|
30
|
+
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]{1,9})?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$/.test(result) || !validDay(result.slice(0, 10)) || !Number.isFinite(Date.parse(result)))
|
|
31
|
+
throw Error();
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
function values(value, cap) {
|
|
35
|
+
if (!Array.isArray(value) || value.length > cap)
|
|
36
|
+
throw Error();
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function item(value, bucket) {
|
|
40
|
+
const r = row(value);
|
|
41
|
+
if (r.status !== "pending" || r.bucket !== bucket || (r.due_date === undefined) === (r.due_at === undefined))
|
|
42
|
+
throw Error();
|
|
43
|
+
let when;
|
|
44
|
+
if (r.due_date !== undefined) {
|
|
45
|
+
if (!validDay(r.due_date))
|
|
46
|
+
throw Error();
|
|
47
|
+
when = { due_date: r.due_date };
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
when = { due_at: timestamp(r.due_at) };
|
|
51
|
+
const project = r.project_id === undefined ? {} : { project_id: identity(r.project_id), project_title: projectTitle(r.project_title) };
|
|
52
|
+
if (r.project_id === undefined && r.project_title !== undefined)
|
|
53
|
+
throw Error();
|
|
54
|
+
if (r.project_unavailable_reason !== undefined && r.project_unavailable_reason !== "project_unavailable")
|
|
55
|
+
throw Error();
|
|
56
|
+
return { id: identity(r.id), title: text(r.title, 80), status: "pending", bucket, ...when, ...project,
|
|
57
|
+
...(r.project_unavailable_reason === undefined ? {} : { project_unavailable_reason: r.project_unavailable_reason }) };
|
|
58
|
+
}
|
|
59
|
+
function segment(value, bucket) {
|
|
60
|
+
const r = row(value), total = count(r.total), items = values(r.items, 8).map(v => item(v, bucket));
|
|
61
|
+
if (items.length > total || r.has_more !== (items.length < total))
|
|
62
|
+
throw Error();
|
|
63
|
+
return { total, items, has_more: r.has_more };
|
|
64
|
+
}
|
|
65
|
+
function titles(value, cap) {
|
|
66
|
+
const r = row(value), total = count(r.total), titles = values(r.titles, cap).map(v => text(v, 80));
|
|
67
|
+
if (titles.length > total || r.has_more !== (titles.length < total))
|
|
68
|
+
throw Error();
|
|
69
|
+
return { total, titles, has_more: r.has_more };
|
|
70
|
+
}
|
|
71
|
+
export function verifiedScheduleStart(value) {
|
|
72
|
+
const r = row(value);
|
|
73
|
+
if (r.timezone !== "Asia/Shanghai" || r.item_limit !== 20)
|
|
74
|
+
throw Error();
|
|
75
|
+
const overdue = segment(r.overdue, "overdue"), today = segment(r.today, "today"), undated = titles(r.undated, 3), next = titles(r.next_seven_days, 1), total = count(r.total_stored), later = count(r.later_total);
|
|
76
|
+
if (overdue.total + today.total + undated.total + next.total + later > total)
|
|
77
|
+
throw Error();
|
|
78
|
+
return { timezone: "Asia/Shanghai", as_of: timestamp(r.as_of), item_limit: 20, total_stored: total,
|
|
79
|
+
overdue, today, undated, next_seven_days: next, later_total: later };
|
|
80
|
+
}
|
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
2
|
+
import { validateCommandArguments } from "./input-validation.js";
|
|
3
|
+
import { ScheduleProblem, scheduleBody, scheduleFailure, scheduleID, scheduleInvalid, scheduleKey, scheduleQuery, scheduleReadResult, scheduleReceipt, scheduleRoot, scheduleUnknown, validScheduleRevision } from "./schedule-contract.js";
|
|
4
|
+
export async function executeSchedule(args, flags, ctx) {
|
|
5
|
+
try {
|
|
6
|
+
validateCommandArguments(args, flags);
|
|
7
|
+
const operation = args[1] === "add" ? "create" : args[1] === "done" ? "complete" : args[1];
|
|
8
|
+
let path, options = { method: "GET", token: ctx.token }, key;
|
|
9
|
+
const detail = operation === "show";
|
|
10
|
+
if (operation === "list") {
|
|
11
|
+
const names = { view: "view", status: "status", "group-by": "group_by", project: "project_id", q: "q", "start-date": "start_date", "end-date": "end_date", limit: "limit", offset: "offset" };
|
|
12
|
+
const input = {};
|
|
13
|
+
for (const [name, value] of Object.entries(flags)) {
|
|
14
|
+
if (!Object.hasOwn(names, name))
|
|
15
|
+
scheduleInvalid();
|
|
16
|
+
if (name === "limit" || name === "offset") {
|
|
17
|
+
if (typeof value !== "string" || !/^\d+$/.test(value))
|
|
18
|
+
scheduleInvalid();
|
|
19
|
+
input[names[name]] = Number(value);
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
input[names[name]] = value;
|
|
23
|
+
}
|
|
24
|
+
const query = new URLSearchParams(Object.entries(scheduleQuery(input)).map(([name, value]) => [name, String(value)])).toString();
|
|
25
|
+
path = `${scheduleRoot}/m1/schedule-items${query ? `?${query}` : ""}`;
|
|
26
|
+
}
|
|
27
|
+
else if (detail)
|
|
28
|
+
path = `${scheduleRoot}/m1/schedule-items/${scheduleID(args[2])}`;
|
|
29
|
+
else if (operation === "status") {
|
|
30
|
+
key = scheduleKey(flags["idempotency-key"]);
|
|
31
|
+
path = `${scheduleRoot}/m1/schedule-write-status`;
|
|
32
|
+
options.headers = { "Idempotency-Key": key };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (!["create", "update", "complete", "cancel", "undo"].includes(operation))
|
|
36
|
+
return scheduleInvalid();
|
|
37
|
+
key = scheduleKey(flags["idempotency-key"]);
|
|
38
|
+
const create = operation === "create";
|
|
39
|
+
const headers = { "Idempotency-Key": key };
|
|
40
|
+
if (!create) {
|
|
41
|
+
const revision = flags.revision;
|
|
42
|
+
if (typeof revision !== "string" || !/^[1-9]\d*$/.test(revision) || !validScheduleRevision(Number(revision)))
|
|
43
|
+
scheduleInvalid();
|
|
44
|
+
headers["If-Match"] = `"${revision}"`;
|
|
45
|
+
}
|
|
46
|
+
if (typeof flags.input !== "string" || !flags.input.trim())
|
|
47
|
+
scheduleInvalid();
|
|
48
|
+
const body = scheduleBody(operation, await ctx.readInput(flags.input));
|
|
49
|
+
path = `${scheduleRoot}/m2/schedule-items${create ? "" : `/${scheduleID(args[2])}${operation === "update" ? "" : `/${operation}`}`}`;
|
|
50
|
+
options = { ...options, method: operation === "update" ? "PATCH" : "POST", body, headers };
|
|
51
|
+
}
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await ctx.api.request(path, options);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
if (key)
|
|
58
|
+
throw scheduleUnknown(key);
|
|
59
|
+
throw new ScheduleProblem("network_error", "日程读取未完成。", "检查当前执行环境的网络;不要反复重试或重新授权。", 6);
|
|
60
|
+
}
|
|
61
|
+
if (![200, 201].includes(result.status))
|
|
62
|
+
throw scheduleFailure(result.status, result.body, operation === "list" || detail ? ["read_workbench", "read_projects"] : operation === "status" ? [] : ["write_schedule"], key);
|
|
63
|
+
return { ...result, body: key ? scheduleReceipt(result.status, result.body, key) : scheduleReadResult(result.status, result.body, detail) };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof ScheduleProblem)
|
|
67
|
+
throw new CliError(error.code, error.humanMessage, error.nextAction, error.exitCode, error.details);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { productionOrigin } from "./config.js";
|
|
2
|
+
import { getAgentOperations } from "./generated/agent-operations.js";
|
|
3
|
+
// Match the existing closed config origins, not an arbitrary apiFactory URL.
|
|
4
|
+
// This reads public metadata only; it never loads a credential or probes an API.
|
|
5
|
+
function currentOrigin(input) {
|
|
6
|
+
const expected = input.environment === "production" ? productionOrigin : input.environment === "test" ? "https://test.gtcai.cn" : undefined;
|
|
7
|
+
if (!expected || typeof input.baseUrl !== "string" || input.credentialOrigin !== expected || input.baseUrl.trim() !== input.baseUrl)
|
|
8
|
+
return undefined;
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(input.baseUrl);
|
|
11
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" || url.origin !== expected)
|
|
12
|
+
return undefined;
|
|
13
|
+
return url.origin;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function setupGuidance(input) {
|
|
20
|
+
const operations = getAgentOperations(), origin = currentOrigin(input);
|
|
21
|
+
const verified = input.connectionVerified === true && origin !== undefined;
|
|
22
|
+
const name = typeof input.agentName === "string" && input.agentName.trim() === input.agentName && [...input.agentName].length >= 1 && [...input.agentName].length <= 80
|
|
23
|
+
&& !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(input.agentName) ? input.agentName : undefined;
|
|
24
|
+
const scope = verified && (input.scope === "read_only" || input.scope === "read_write") ? input.scope : "unknown";
|
|
25
|
+
return {
|
|
26
|
+
schema_version: 1,
|
|
27
|
+
user_experience: { state: "unknown", basis: "not_recorded" },
|
|
28
|
+
agent_preparation: {
|
|
29
|
+
status: "required",
|
|
30
|
+
body_obtained: "unknown",
|
|
31
|
+
evidence: "not_observable_by_cli",
|
|
32
|
+
operations: { version: operations.version, sha256: operations.sha256, source: "package_generated", command: "caotang guide", manual_path: operations.manual_path },
|
|
33
|
+
},
|
|
34
|
+
presentation_reason: input.ready === true && verified ? "requested" : "not_ready",
|
|
35
|
+
identity: { verified, ...(verified && name !== undefined ? { agent_name: name } : {}), ...(origin ? { environment: input.environment, origin } : {}) },
|
|
36
|
+
permissions: { scope, account_capabilities: "unknown", effective_capabilities: "unknown" },
|
|
37
|
+
entry: { transport: "cli", skill_support: "unknown" },
|
|
38
|
+
...(origin ? { manual_url: new URL(operations.manual_path, origin).href } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/skill-assets.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { asRecord, CliError } from "./errors.js";
|
|
8
8
|
// One release path allowlist, checked against the real package manifest.
|
|
9
9
|
// Never use an arbitrary manifest entry as a filesystem installation target.
|
|
10
|
-
export const skillNames = Object.freeze(["caotang-context", "caotang-save-knowledge", "caotang-project-progress", "caotang-search", "caotang-workday", "caotang-first-connect"]);
|
|
10
|
+
export const skillNames = Object.freeze(["caotang-context", "caotang-save-knowledge", "caotang-project-progress", "caotang-search", "caotang-workday", "caotang-first-connect", "caotang-schedule"]);
|
|
11
11
|
export function completeSkillInventory(names) {
|
|
12
12
|
return names.length === skillNames.length && new Set(names).size === skillNames.length && names.every(name => skillNames.includes(name));
|
|
13
13
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants, closeSync, fstatSync, fsyncSync, linkSync, lstatSync, openSync, readSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { asRecord, CliError } from "./errors.js";
|
|
5
|
+
import { getAgentOperations } from "./generated/agent-operations.js";
|
|
6
|
+
import { loadSkillAssets } from "./skill-assets.js";
|
|
7
|
+
const unsafe = () => new CliError("skills_export_path_unsafe", "导出位置未通过安全检查。", "请明确指定一个新ZIP文件,父目录须已存在、属于本人且不是符号链接;不会自动创建目录、改权限或覆盖文件。", 2);
|
|
8
|
+
const exists = () => new CliError("skills_export_exists", "导出目标已存在,未覆盖。", "保留原文件;请本人明确选择一个新的ZIP文件路径。", 2);
|
|
9
|
+
const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
10
|
+
const missing = (error) => asRecord(error)?.code === "ENOENT";
|
|
11
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
12
|
+
const crcTable = Uint32Array.from({ length: 256 }, (_, index) => {
|
|
13
|
+
let value = index;
|
|
14
|
+
for (let bit = 0; bit < 8; bit++)
|
|
15
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
16
|
+
return value >>> 0;
|
|
17
|
+
});
|
|
18
|
+
function crc32(bytes) {
|
|
19
|
+
let crc = 0xffffffff;
|
|
20
|
+
for (const byte of bytes)
|
|
21
|
+
crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
22
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
function zip(entries) {
|
|
25
|
+
if (entries.length > 32 || entries.reduce((total, entry) => total + entry.content.length, 0) > 1048576)
|
|
26
|
+
throw unsafe();
|
|
27
|
+
const local = [], central = [];
|
|
28
|
+
let offset = 0;
|
|
29
|
+
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
30
|
+
if (!/^(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_.-]+$/.test(entry.name) || entry.name.split("/").some(part => part === "." || part === ".."))
|
|
31
|
+
throw unsafe();
|
|
32
|
+
const name = Buffer.from(entry.name, "utf8"), crc = crc32(entry.content);
|
|
33
|
+
const header = Buffer.alloc(30);
|
|
34
|
+
header.writeUInt32LE(0x04034b50);
|
|
35
|
+
header.writeUInt16LE(20, 4);
|
|
36
|
+
header.writeUInt16LE(0x0800, 6);
|
|
37
|
+
header.writeUInt16LE(0x0021, 12); // Fixed DOS 1980-01-01; no local identity/time metadata.
|
|
38
|
+
header.writeUInt32LE(crc, 14);
|
|
39
|
+
header.writeUInt32LE(entry.content.length, 18);
|
|
40
|
+
header.writeUInt32LE(entry.content.length, 22);
|
|
41
|
+
header.writeUInt16LE(name.length, 26);
|
|
42
|
+
const directory = Buffer.alloc(46);
|
|
43
|
+
directory.writeUInt32LE(0x02014b50);
|
|
44
|
+
directory.writeUInt16LE(0x0314, 4);
|
|
45
|
+
directory.writeUInt16LE(20, 6);
|
|
46
|
+
directory.writeUInt16LE(0x0800, 8);
|
|
47
|
+
directory.writeUInt16LE(0x0021, 14);
|
|
48
|
+
directory.writeUInt32LE(crc, 16);
|
|
49
|
+
directory.writeUInt32LE(entry.content.length, 20);
|
|
50
|
+
directory.writeUInt32LE(entry.content.length, 24);
|
|
51
|
+
directory.writeUInt16LE(name.length, 28);
|
|
52
|
+
directory.writeUInt32LE((0o100600 << 16) >>> 0, 38);
|
|
53
|
+
directory.writeUInt32LE(offset, 42);
|
|
54
|
+
local.push(header, name, entry.content);
|
|
55
|
+
central.push(directory, name);
|
|
56
|
+
offset += header.length + name.length + entry.content.length;
|
|
57
|
+
}
|
|
58
|
+
const directory = Buffer.concat(central), end = Buffer.alloc(22);
|
|
59
|
+
end.writeUInt32LE(0x06054b50);
|
|
60
|
+
end.writeUInt16LE(entries.length, 8);
|
|
61
|
+
end.writeUInt16LE(entries.length, 10);
|
|
62
|
+
end.writeUInt32LE(directory.length, 12);
|
|
63
|
+
end.writeUInt32LE(offset, 16);
|
|
64
|
+
return Buffer.concat([...local, directory, end]);
|
|
65
|
+
}
|
|
66
|
+
function outputPath(value) {
|
|
67
|
+
if (typeof value !== "string" || !value.trim() || value.length > 4096 || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value))
|
|
68
|
+
throw unsafe();
|
|
69
|
+
const target = resolve(value);
|
|
70
|
+
if (!isAbsolute(target) || target === parse(target).root || !basename(target).toLowerCase().endsWith(".zip"))
|
|
71
|
+
throw unsafe();
|
|
72
|
+
return target;
|
|
73
|
+
}
|
|
74
|
+
// Public ZIP output is not restricted to HOME. All existing ancestors must be
|
|
75
|
+
// real directories; only trusted root/sticky system ancestors may precede the
|
|
76
|
+
// user's private final parent. No directory contents are scanned or modified.
|
|
77
|
+
function checkedParent(path) {
|
|
78
|
+
if (typeof process.getuid !== "function")
|
|
79
|
+
throw unsafe();
|
|
80
|
+
const uid = process.getuid(), parent = dirname(path), root = parse(parent).root;
|
|
81
|
+
let current = root, result;
|
|
82
|
+
for (const part of [null, ...relative(root, parent).split(sep).filter(Boolean)]) {
|
|
83
|
+
if (part !== null)
|
|
84
|
+
current = join(current, part);
|
|
85
|
+
const info = lstatSync(current), final = current === parent;
|
|
86
|
+
const stickyRootAncestor = !final && info.uid === 0 && (info.mode & 0o1000) !== 0;
|
|
87
|
+
if (!info.isDirectory() || (final ? info.uid !== uid : info.uid !== uid && info.uid !== 0)
|
|
88
|
+
|| (info.mode & 0o022) !== 0 && !stickyRootAncestor)
|
|
89
|
+
throw unsafe();
|
|
90
|
+
result = info;
|
|
91
|
+
}
|
|
92
|
+
if (!result || realpathSync(parent) !== parent)
|
|
93
|
+
throw unsafe();
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function readDescriptor(fd, length) {
|
|
97
|
+
const bytes = Buffer.alloc(length);
|
|
98
|
+
let offset = 0;
|
|
99
|
+
while (offset < length) {
|
|
100
|
+
const count = readSync(fd, bytes, offset, length - offset, offset);
|
|
101
|
+
if (!count)
|
|
102
|
+
break;
|
|
103
|
+
offset += count;
|
|
104
|
+
}
|
|
105
|
+
return bytes.subarray(0, offset);
|
|
106
|
+
}
|
|
107
|
+
function publish(path, bytes) {
|
|
108
|
+
const parent = dirname(path), identity = checkedParent(path), original = process.cwd();
|
|
109
|
+
let fd, stagedIdentity, temporary, published = false;
|
|
110
|
+
try {
|
|
111
|
+
// Keep all operations synchronous while pinned to the verified parent inode.
|
|
112
|
+
process.chdir(parent);
|
|
113
|
+
const current = lstatSync(".");
|
|
114
|
+
if (!same(current, identity) || current.uid !== identity.uid || current.mode !== identity.mode)
|
|
115
|
+
throw unsafe();
|
|
116
|
+
const name = basename(path);
|
|
117
|
+
try {
|
|
118
|
+
lstatSync(name);
|
|
119
|
+
throw exists();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!missing(error))
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
temporary = `.caotang-skills-export-${randomUUID()}.tmp`;
|
|
126
|
+
fd = openSync(temporary, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
127
|
+
stagedIdentity = fstatSync(fd);
|
|
128
|
+
const valid = (info, links) => info.isFile() && info.uid === identity.uid && info.nlink === links && (info.mode & 0o7777) === 0o600 && same(info, stagedIdentity);
|
|
129
|
+
if (!valid(stagedIdentity, 1) || stagedIdentity.size !== 0)
|
|
130
|
+
throw unsafe();
|
|
131
|
+
writeFileSync(fd, bytes);
|
|
132
|
+
fsyncSync(fd);
|
|
133
|
+
const written = fstatSync(fd);
|
|
134
|
+
if (!valid(written, 1) || written.size !== bytes.length || !same(written, lstatSync(temporary)) || !readDescriptor(fd, bytes.length).equals(bytes))
|
|
135
|
+
throw unsafe();
|
|
136
|
+
try {
|
|
137
|
+
linkSync(temporary, name);
|
|
138
|
+
published = true;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (asRecord(error)?.code === "EEXIST")
|
|
142
|
+
throw exists();
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
if (!valid(lstatSync(name), 2) || !same(lstatSync(temporary), stagedIdentity))
|
|
146
|
+
throw unsafe();
|
|
147
|
+
unlinkSync(temporary);
|
|
148
|
+
temporary = undefined;
|
|
149
|
+
const final = lstatSync(name);
|
|
150
|
+
if (!valid(final, 1) || final.size !== bytes.length || !readDescriptor(fd, bytes.length).equals(bytes))
|
|
151
|
+
throw unsafe();
|
|
152
|
+
const after = lstatSync(".");
|
|
153
|
+
if (!same(after, identity) || after.uid !== identity.uid || after.mode !== identity.mode)
|
|
154
|
+
throw unsafe();
|
|
155
|
+
// cwd remains the old inode after a concurrent rename. Also verify the
|
|
156
|
+
// absolute location promised to the caller, including its safe ancestry.
|
|
157
|
+
const promisedParent = checkedParent(path), promisedTarget = lstatSync(path);
|
|
158
|
+
if (!same(promisedParent, identity) || promisedParent.uid !== identity.uid || promisedParent.mode !== identity.mode
|
|
159
|
+
|| !valid(promisedTarget, 1) || promisedTarget.size !== bytes.length || !same(promisedTarget, final)
|
|
160
|
+
|| !readDescriptor(fd, bytes.length).equals(bytes))
|
|
161
|
+
throw unsafe();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (published)
|
|
165
|
+
throw new CliError("skills_export_unconfirmed", "已创建导出目标,但最终核对未完成。", "保留现有ZIP并由本人核对文件;不要自动删除、覆盖或重新导出到同一路径。", 6);
|
|
166
|
+
if (error instanceof CliError)
|
|
167
|
+
throw error;
|
|
168
|
+
throw unsafe();
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (fd !== undefined)
|
|
172
|
+
closeSync(fd);
|
|
173
|
+
if (temporary && stagedIdentity) {
|
|
174
|
+
try {
|
|
175
|
+
if (same(lstatSync(temporary), stagedIdentity))
|
|
176
|
+
unlinkSync(temporary);
|
|
177
|
+
}
|
|
178
|
+
catch { /* Preserve an unverified pathname; never remove another inode. */ }
|
|
179
|
+
}
|
|
180
|
+
process.chdir(original);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export async function exportSkillsBundle(options) {
|
|
184
|
+
const path = outputPath(options.outputPath);
|
|
185
|
+
const assets = await loadSkillAssets(options.packageRoot);
|
|
186
|
+
if (assets.some(asset => asset.content.includes("本技能使用的 AI 名字是")))
|
|
187
|
+
throw new CliError("skills_export_source_invalid", "本包技能含绑定身份,未导出。", "只使用官方未绑定个人身份的技能包,不从已安装个人目录导出。", 7);
|
|
188
|
+
const operations = getAgentOperations();
|
|
189
|
+
const manifest = { formatVersion: 1, skills: assets.map(({ name, description }) => ({ name, description })) };
|
|
190
|
+
const entries = [
|
|
191
|
+
{ name: "skills/manifest.json", content: Buffer.from(JSON.stringify(manifest, null, 2) + "\n") },
|
|
192
|
+
...assets.map(asset => ({ name: `skills/${asset.name}/SKILL.md`, content: Buffer.from(asset.content) })),
|
|
193
|
+
{ name: "operations.md", content: Buffer.from(operations.content) },
|
|
194
|
+
{ name: "README.md", content: Buffer.from(`# 一间草堂官方公共技能包\n\n七项技能来自本CLI包内官方资源,未绑定个人身份;不含账号、令牌、同意或个人配置。\n公共短合同版本:${operations.version};正文SHA256:${operations.sha256}。\n人用手册:当前服务的 ${operations.manual_path}。\n\n导出不等于安装、授权、当前会话加载或行为通过。请按目标宿主实际支持的导入方式使用,核对共享范围;不会自动上传共享库、启动Agent或改个人配置。\n能执行CLI但无Skill的入口仍可按公共说明使用完整业务能力;纯网页无外部工具的AI尚未接通。\n`) },
|
|
195
|
+
];
|
|
196
|
+
const bytes = zip(entries);
|
|
197
|
+
try {
|
|
198
|
+
publish(path, bytes);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error instanceof CliError)
|
|
202
|
+
throw error;
|
|
203
|
+
throw unsafe();
|
|
204
|
+
}
|
|
205
|
+
return { status: 200, body: { operation: "skills_export", output_path: path, format: "zip", compression: "store", size_bytes: bytes.length, sha256: digest(bytes), entry_count: entries.length, skill_count: assets.length, uploaded: false } };
|
|
206
|
+
}
|
package/dist/web-citations.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
2
2
|
const uuid = (v) => typeof v === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}(?![\s\S])/i.test(v);
|
|
3
3
|
const publicOrigins = ["https://test.gtcai.cn", "https://caotang.gtcai.cn"];
|
|
4
|
-
const gapCaps = { search_projects: "read_projects", search_knowledge: "read_knowledge", knowledge: "read_knowledge", recent_knowledge: "read_knowledge", draft_knowledge: "read_drafts", recent_projects: "read_projects", project_updates: "read_projects", draft_projects: "read_projects", project_drafts: "read_drafts", project_draft_checkins: "read_drafts", project_evidence: "read_projects", draft_project_evidence: "read_drafts", knowledge_applied_updates: "read_knowledge", draft_knowledge_applied_updates: "read_drafts", knowledge_result: "read_knowledge", draft_knowledge_result: "read_drafts", project_result: "read_projects", draft_project_result: "read_drafts" };
|
|
4
|
+
const gapCaps = { search_projects: "read_projects", search_knowledge: "read_knowledge", search_schedule_items: "read_workbench", knowledge: "read_knowledge", recent_knowledge: "read_knowledge", draft_knowledge: "read_drafts", recent_projects: "read_projects", project_updates: "read_projects", draft_projects: "read_projects", project_drafts: "read_drafts", project_draft_checkins: "read_drafts", project_evidence: "read_projects", draft_project_evidence: "read_drafts", knowledge_applied_updates: "read_knowledge", draft_knowledge_applied_updates: "read_drafts", knowledge_result: "read_knowledge", draft_knowledge_result: "read_drafts", project_result: "read_projects", draft_project_result: "read_drafts" };
|
|
5
5
|
function withoutURL(row) { if (!Object.hasOwn(row, "web_url"))
|
|
6
6
|
return row; const { web_url: _ignored, ...data } = row; return data; }
|
|
7
7
|
export function citationOrigin(api, configured) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { verifiedWorkContext } from "./work-context-wire.js";
|
|
2
|
+
const row = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
3
|
+
const clean = (v, limit = 60) => {
|
|
4
|
+
if (typeof v !== "string")
|
|
5
|
+
return "";
|
|
6
|
+
const chars = [...v.replace(/[\p{Cc}\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu, " ").replace(/\s+/gu, " ").trim()];
|
|
7
|
+
return chars.length > limit ? chars.slice(0, limit).join("") + "…" : chars.join("");
|
|
8
|
+
};
|
|
9
|
+
function unavailable(section) {
|
|
10
|
+
if (section?.status === "disabled")
|
|
11
|
+
return "未读取(没有权限)";
|
|
12
|
+
if (section?.status === "read_failed")
|
|
13
|
+
return "本次读取未完成";
|
|
14
|
+
if (section?.status === "deferred")
|
|
15
|
+
return section.reason === "budget_limited" ? "部分内容延后展示" : "本次尚未提供";
|
|
16
|
+
if (section?.status === "not_recorded")
|
|
17
|
+
return "尚未记录";
|
|
18
|
+
if (section?.status === "not_applicable")
|
|
19
|
+
return "本次不适用";
|
|
20
|
+
return "未核实";
|
|
21
|
+
}
|
|
22
|
+
function total(section) {
|
|
23
|
+
return (section?.status === "available" || section?.status === "empty") && typeof section.total === "number" ? String(section.total) : unavailable(section);
|
|
24
|
+
}
|
|
25
|
+
// Human display only. Agents retain the default complete JSON for rules and scope.
|
|
26
|
+
export function workContextHuman(value) {
|
|
27
|
+
try {
|
|
28
|
+
const source = row(value), sourceContext = row(source?.context), selection = row(row(sourceContext?.current_project)?.data);
|
|
29
|
+
const selected = typeof selection?.project_id === "string" ? selection.project_id : undefined;
|
|
30
|
+
const verified = verifiedWorkContext(value, selected);
|
|
31
|
+
if (!verified)
|
|
32
|
+
return undefined;
|
|
33
|
+
const context = verified.context, overview = row(context.project_overview), projects = row(context.projects), current = row(context.current_project), counts = row(overview?.data);
|
|
34
|
+
const at = new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(new Date(verified.as_of));
|
|
35
|
+
const header = counts ? `可读项目 ${counts.total},进行中 ${counts.active},核心 ${counts.core}${counts.draft === null ? "(草稿未计入)" : ""}` : `项目:${unavailable(overview)}`;
|
|
36
|
+
const lines = [`草堂摘要 · ${at} · ${header}`];
|
|
37
|
+
const list = Array.isArray(projects?.data) ? projects.data : [];
|
|
38
|
+
if (current?.status === "available" && list.length) {
|
|
39
|
+
const project = list[0], next = row(project.next_step), step = row(next?.data);
|
|
40
|
+
lines.push(`本次:${clean(project.title)};上次留下的下一步:${step ? clean(step.summary, 100) : "未留下明确记录"}`);
|
|
41
|
+
}
|
|
42
|
+
else if (list.length) {
|
|
43
|
+
lines.push(`核心/近期:${list.slice(0, 2).map(p => clean(p.title)).join("、")}${list.length > 2 ? `;另有 ${list.length - 2} 个候选` : ""}。`);
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
lines.push(`项目详情:${projects?.status === "empty" ? "当前可读范围内暂无项目" : unavailable(projects)}`);
|
|
47
|
+
const schedule = row(context.schedule_items), data = row(schedule?.data);
|
|
48
|
+
if (schedule?.status === "available" && data) {
|
|
49
|
+
const overdue = row(data.overdue), today = row(data.today), undated = row(data.undated);
|
|
50
|
+
const urgent = [...overdue.items, ...today.items].slice(0, 3).map(p => clean(p.title, 40));
|
|
51
|
+
lines.push(`待办:今日 ${today.total},逾期 ${overdue.total},未定日期 ${undated.total}${urgent.length ? `;优先:${urgent.join("、")}` : ""}`);
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
lines.push(`待办:${unavailable(schedule)}`);
|
|
55
|
+
const rules = row(context.rules), pending = row(context.pending), notices = row(context.official_notices), noticeData = row(notices?.data);
|
|
56
|
+
lines.push(`待确认:${total(pending)}${typeof pending?.total === "number" ? " 项(网页核对)" : ""};规矩:${total(rules)}${typeof rules?.total === "number" ? " 条(原文见 JSON)" : ""};通知:${noticeData ? noticeData.unread_count : unavailable(notices)}`);
|
|
57
|
+
const connections = row(context.recent_connections), reminders = row(context.conditional_reminders);
|
|
58
|
+
const connectionText = connections?.status === "available" || connections?.status === "empty" ? `${connections.total} 条近期记录` : unavailable(connections);
|
|
59
|
+
const reminderData = row(reminders?.data);
|
|
60
|
+
const reminderText = reminderData ? `项目 ${total(row(reminderData.projects))},知识 ${total(row(reminderData.knowledge))}` : unavailable(reminders);
|
|
61
|
+
lines.push(`协作:${connectionText};整理提示:${reminderText}。完整内容可去掉 --human 查看。`);
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|