@kyun.si/agent2cursor 0.0.0-stage → 0.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.
Files changed (113) hide show
  1. package/CONTRIBUTING.md +89 -0
  2. package/LICENSE +21 -0
  3. package/README.md +202 -2
  4. package/README.zh-CN.md +203 -0
  5. package/THIRD_PARTY_NOTICES.md +11 -0
  6. package/demos/full-send/LICENSE-tstack +21 -0
  7. package/demos/full-send/README.md +47 -0
  8. package/demos/full-send/TSTACK-PROVENANCE.json +37 -0
  9. package/demos/full-send/bot/agent.ts +9 -0
  10. package/demos/full-send/bot/channels/github.ts +16 -0
  11. package/demos/full-send/bot/instructions.md +11 -0
  12. package/demos/full-send/bot/lib/merge-policy.ts +9 -0
  13. package/demos/full-send/bot/lib/settings.ts +2 -0
  14. package/demos/full-send/bot/skills/babysit-pr/SKILL.md +13 -0
  15. package/demos/full-send/bot/skills/babysit-pr/scripts/pr-wait +556 -0
  16. package/demos/full-send/bot/skills/file-pr/SKILL.md +11 -0
  17. package/demos/full-send/bot/skills/full-send/SKILL.md +19 -0
  18. package/demos/full-send/bot/skills/leaving-pr-comments/SKILL.md +14 -0
  19. package/demos/full-send/bot/skills/sol-loop/SKILL.md +11 -0
  20. package/demos/full-send/bot/subagents/reviewer/agent.ts +2 -0
  21. package/demos/full-send/bot/subagents/reviewer/instructions.md +3 -0
  22. package/demos/full-send/bot/tools/merge_pr.ts +23 -0
  23. package/demos/full-send/bot/tools/pr_wait.ts +24 -0
  24. package/demos/full-send/bot/tools/pr_wait_script.ts +9 -0
  25. package/demos/full-send/evals/evals.config.ts +2 -0
  26. package/demos/full-send/evals/full-send.eval.ts +5 -0
  27. package/demos/full-send/evals/skills.eval.ts +8 -0
  28. package/demos/full-send/package.json +18 -0
  29. package/demos/pr-review/LICENSE-tstack +21 -0
  30. package/demos/pr-review/README.md +44 -0
  31. package/demos/pr-review/TSTACK-PROVENANCE.json +37 -0
  32. package/demos/pr-review/bot/agent.ts +9 -0
  33. package/demos/pr-review/bot/channels/github.ts +22 -0
  34. package/demos/pr-review/bot/instructions.md +11 -0
  35. package/demos/pr-review/bot/lib/pr-snapshot.ts +92 -0
  36. package/demos/pr-review/bot/lib/settings.ts +2 -0
  37. package/demos/pr-review/bot/skills/council/SKILL.md +25 -0
  38. package/demos/pr-review/bot/skills/leaving-pr-comments/SKILL.md +14 -0
  39. package/demos/pr-review/bot/subagents/reviewer-a/agent.ts +2 -0
  40. package/demos/pr-review/bot/subagents/reviewer-a/instructions.md +3 -0
  41. package/demos/pr-review/bot/subagents/reviewer-b/agent.ts +2 -0
  42. package/demos/pr-review/bot/subagents/reviewer-b/instructions.md +3 -0
  43. package/demos/pr-review/bot/subagents/reviewer-c/agent.ts +2 -0
  44. package/demos/pr-review/bot/subagents/reviewer-c/instructions.md +3 -0
  45. package/demos/pr-review/bot/tools/pr_wait.ts +24 -0
  46. package/demos/pr-review/evals/evals.config.ts +2 -0
  47. package/demos/pr-review/evals/pr-review.eval.ts +13 -0
  48. package/demos/pr-review/evals/skills.eval.ts +5 -0
  49. package/demos/pr-review/package.json +18 -0
  50. package/dist/cli.d.ts +2 -0
  51. package/dist/cli.js +142 -0
  52. package/dist/emit/common.d.ts +4 -0
  53. package/dist/emit/common.js +15 -0
  54. package/dist/emit/evals.d.ts +4 -0
  55. package/dist/emit/evals.js +33 -0
  56. package/dist/emit/project.d.ts +2 -0
  57. package/dist/emit/project.js +165 -0
  58. package/dist/emit/report.d.ts +2 -0
  59. package/dist/emit/report.js +74 -0
  60. package/dist/emit/subagent.d.ts +2 -0
  61. package/dist/emit/subagent.js +15 -0
  62. package/dist/emit/tool-stub.d.ts +3 -0
  63. package/dist/emit/tool-stub.js +29 -0
  64. package/dist/emit/tstack-license.d.ts +8 -0
  65. package/dist/emit/tstack-license.js +39 -0
  66. package/dist/ir.d.ts +66 -0
  67. package/dist/ir.js +1 -0
  68. package/dist/mapping/codex-agent.d.ts +2 -0
  69. package/dist/mapping/codex-agent.js +39 -0
  70. package/dist/mapping/codex-skill.d.ts +5 -0
  71. package/dist/mapping/codex-skill.js +32 -0
  72. package/dist/mapping/foreign-tools.d.ts +2 -0
  73. package/dist/mapping/foreign-tools.js +33 -0
  74. package/dist/mapping/model.d.ts +5 -0
  75. package/dist/mapping/model.js +14 -0
  76. package/dist/mapping/names.d.ts +2 -0
  77. package/dist/mapping/names.js +9 -0
  78. package/dist/mapping/permissions.d.ts +2 -0
  79. package/dist/mapping/permissions.js +21 -0
  80. package/dist/mapping/skill.d.ts +3 -0
  81. package/dist/mapping/skill.js +29 -0
  82. package/dist/mapping/subagent.d.ts +3 -0
  83. package/dist/mapping/subagent.js +60 -0
  84. package/dist/mapping/tools-names.d.ts +6 -0
  85. package/dist/mapping/tools-names.js +18 -0
  86. package/dist/scan/codex.d.ts +1 -0
  87. package/dist/scan/codex.js +19 -0
  88. package/dist/scan/frontmatter.d.ts +11 -0
  89. package/dist/scan/frontmatter.js +62 -0
  90. package/dist/scan/index.d.ts +2 -0
  91. package/dist/scan/index.js +192 -0
  92. package/dist/scan/safety.d.ts +11 -0
  93. package/dist/scan/safety.js +105 -0
  94. package/dist/scan/tstack.d.ts +9 -0
  95. package/dist/scan/tstack.js +42 -0
  96. package/dist/validate.d.ts +5 -0
  97. package/dist/validate.js +135 -0
  98. package/dist/write.d.ts +19 -0
  99. package/dist/write.js +134 -0
  100. package/docs/acceptance.md +86 -0
  101. package/docs/assets/agent2cursor-demo-poster.png +0 -0
  102. package/docs/assets/agent2cursor-demo.gif +0 -0
  103. package/docs/evidence/codex-adapter.json +133 -0
  104. package/docs/evidence/evaluation-fixes.json +184 -0
  105. package/docs/evidence/independent-evaluation.json +61 -0
  106. package/docs/evidence/online-2026-10-27.json +112 -0
  107. package/docs/official-examples.provenance.json +26 -0
  108. package/docs/upstream-verification.md +67 -0
  109. package/evaluation/20261027-2216//345/244/215/346/265/213/347/273/223/350/256/272.md +48 -0
  110. package/evaluation/README.md +16 -0
  111. package/package.json +30 -4
  112. package/templates/en/instructions.md +9 -0
  113. package/templates/zh/instructions.md +9 -0
@@ -0,0 +1,105 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, readdir, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ export const MAX_TEXT_BYTES = 1024 * 1024;
5
+ export const MAX_ASSET_BYTES = 16 * 1024 * 1024;
6
+ export const MAX_FILES = 5000;
7
+ export const SKIP_DIRECTORIES = new Set([".git", "node_modules", ".validation", "dist", "build", ".cache", "__pycache__"]);
8
+ export function isSensitiveName(name) {
9
+ return /^(?:\.env(?:\..*)?|\.envrc|\.npmrc|\.netrc|\.pypirc|\.git-credentials|\.ssh|\.aws|\.kube|\.?credentials?(?:\..*)?|\.?secrets?(?:\..*)?|\.?tokens?(?:\..*)?|\.?auth(?:entication)?\.(?:json|ya?ml|toml)|cookies?(?:\..*)?|id_(?:rsa|dsa|ed25519|ecdsa)(?:\..*)?)$/i.test(name)
10
+ || /\.(?:pem|p12|pfx|key|keystore)$/i.test(name);
11
+ }
12
+ /** Never resolve a symlink before deciding whether it is safe to read. */
13
+ export async function assertSafePath(file) {
14
+ let absolute = path.resolve(file);
15
+ // Match the writer's treatment of platform-owned macOS aliases. User-created
16
+ // symlinks anywhere after these aliases are still rejected component by component.
17
+ if (process.platform === "darwin") {
18
+ for (const alias of ["/var", "/tmp"]) {
19
+ if (absolute === alias || absolute.startsWith(`${alias}/`)) {
20
+ absolute = path.join(await realpath(alias), path.relative(alias, absolute));
21
+ break;
22
+ }
23
+ }
24
+ }
25
+ const parts = absolute.slice(path.parse(absolute).root.length).split(path.sep).filter(Boolean);
26
+ if (parts.some(isSensitiveName))
27
+ throw new Error(`Sensitive input path is not permitted: ${absolute}`);
28
+ if (parts.some((part) => /clash/i.test(part)) && /\.ya?ml$/i.test(absolute)) {
29
+ throw new Error("Reading local Clash YAML is not permitted.");
30
+ }
31
+ let current = path.parse(absolute).root;
32
+ for (const part of parts) {
33
+ current = path.join(current, part);
34
+ const stat = await lstat(current);
35
+ if (stat.isSymbolicLink())
36
+ throw new Error(`Symbolic links are not permitted: ${current}`);
37
+ }
38
+ return absolute;
39
+ }
40
+ export async function readSafeFile(file, maxBytes = MAX_ASSET_BYTES) {
41
+ const absolute = await assertSafePath(file);
42
+ const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
43
+ try {
44
+ const stat = await handle.stat();
45
+ if (!stat.isFile())
46
+ throw new Error(`Expected a regular file: ${absolute}`);
47
+ if (stat.size > maxBytes)
48
+ throw new Error(`Input exceeds ${maxBytes} bytes: ${absolute}`);
49
+ // Bounded reads also handle files growing after stat(), without loading the excess.
50
+ const buffer = Buffer.alloc(Math.min(stat.size + 1, maxBytes + 1));
51
+ let length = 0;
52
+ while (length < buffer.length) {
53
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
54
+ if (!bytesRead)
55
+ break;
56
+ length += bytesRead;
57
+ }
58
+ if (length > stat.size || length > maxBytes)
59
+ throw new Error(`Input changed or exceeded its size limit: ${absolute}`);
60
+ return buffer.subarray(0, length);
61
+ }
62
+ finally {
63
+ await handle.close();
64
+ }
65
+ }
66
+ export async function readSafeText(file, maxBytes = MAX_TEXT_BYTES) {
67
+ const bytes = await readSafeFile(file, maxBytes);
68
+ if (bytes.includes(0))
69
+ throw new Error(`Expected UTF-8 text: ${path.resolve(file)}`);
70
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
71
+ }
72
+ /** Absolute, sorted regular files. Sensitive files fail closed instead of being copied. */
73
+ export async function listSafeFiles(directory, onSkip) {
74
+ const root = await assertSafePath(directory);
75
+ if (!(await lstat(root)).isDirectory())
76
+ throw new Error(`Expected a directory: ${root}`);
77
+ const files = [];
78
+ async function walk(dir, depth) {
79
+ if (depth > 32)
80
+ throw new Error(`Input nesting exceeds 32 directories: ${root}`);
81
+ for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
82
+ const file = path.join(dir, entry.name);
83
+ if (entry.isSymbolicLink())
84
+ throw new Error(`Symbolic links are not permitted: ${file}`);
85
+ if (isSensitiveName(entry.name))
86
+ throw new Error(`Sensitive input path is not permitted: ${file}`);
87
+ if (entry.isDirectory()) {
88
+ if (!SKIP_DIRECTORIES.has(entry.name))
89
+ await walk(file, depth + 1);
90
+ else
91
+ onSkip?.(file, "Generated, dependency, cache, or version-control directory omitted.");
92
+ }
93
+ else if (entry.isFile()) {
94
+ if (files.length >= MAX_FILES)
95
+ throw new Error(`Input exceeds ${MAX_FILES} files: ${root}`);
96
+ files.push(file);
97
+ }
98
+ else {
99
+ throw new Error(`Special files are not permitted: ${file}`);
100
+ }
101
+ }
102
+ }
103
+ await walk(root, 0);
104
+ return files;
105
+ }
@@ -0,0 +1,9 @@
1
+ export declare const TSTACK_COMMIT = "7de017b3fc9d50c03cf3ecb98fb71e0ca65a78bd";
2
+ /** Verified against the README at the pinned commit, also stored in fixtures/tstack. */
3
+ export declare const TSTACK_DEPENDENCIES: Readonly<Record<string, readonly string[]>>;
4
+ export declare function parseTstackDependencies(readme: string): Record<string, string[]>;
5
+ export declare function dependencyInfo(directory: string, id: string): Promise<{
6
+ names: readonly string[];
7
+ basis: string;
8
+ fallback: boolean;
9
+ }>;
@@ -0,0 +1,42 @@
1
+ import path from "node:path";
2
+ import { lstat } from "node:fs/promises";
3
+ import { readSafeText } from "./safety.js";
4
+ export const TSTACK_COMMIT = "7de017b3fc9d50c03cf3ecb98fb71e0ca65a78bd";
5
+ /** Verified against the README at the pinned commit, also stored in fixtures/tstack. */
6
+ export const TSTACK_DEPENDENCIES = {
7
+ "full-send": ["sol-loop", "file-pr", "babysit-pr"],
8
+ "full-stop": ["full-send"],
9
+ takeover: ["modernize", "file-pr", "babysit-pr", "leaving-pr-comments"],
10
+ "file-pr": ["leaving-pr-comments"],
11
+ "babysit-pr": ["leaving-pr-comments"],
12
+ modernize: ["leaving-pr-comments"],
13
+ council: [], "sol-loop": [], "leaving-pr-comments": [],
14
+ };
15
+ export function parseTstackDependencies(readme) {
16
+ const dependencies = Object.create(null);
17
+ for (const line of readme.split(/\r?\n/)) {
18
+ const match = /^-\s+(.+?)\s+uses?\s+(.+)$/.exec(line);
19
+ if (!match)
20
+ continue;
21
+ const sources = [...match[1].matchAll(/`([a-z0-9_-]+)`/g)].map((value) => value[1]);
22
+ const targets = [...match[2].matchAll(/`([a-z0-9_-]+)`/g)].map((value) => value[1]);
23
+ for (const source of sources)
24
+ dependencies[source] = targets;
25
+ }
26
+ return dependencies;
27
+ }
28
+ export async function dependencyInfo(directory, id) {
29
+ const readmePath = path.join(path.dirname(path.dirname(directory)), "README.md");
30
+ try {
31
+ await lstat(readmePath);
32
+ const readme = await readSafeText(readmePath);
33
+ if (/^#\s+tstack\s*$/m.test(readme) && readme.includes("t3dotgg/tstack")) {
34
+ return { names: parseTstackDependencies(readme)[id] ?? [], basis: readmePath, fallback: false };
35
+ }
36
+ }
37
+ catch (error) {
38
+ if (error.code !== "ENOENT")
39
+ throw error;
40
+ }
41
+ return { names: Object.hasOwn(TSTACK_DEPENDENCIES, id) ? TSTACK_DEPENDENCIES[id] : [], basis: `t3dotgg/tstack@${TSTACK_COMMIT}`, fallback: true };
42
+ }
@@ -0,0 +1,5 @@
1
+ import type { Language, ValidationResult } from "./ir.js";
2
+ export declare function supportedNode(version?: string): boolean;
3
+ export declare function findSdk(dir: string): Promise<string | undefined>;
4
+ export declare function validationMessage(result: ValidationResult, lang: Language): string;
5
+ export declare function validateProject(dir: string, sdk?: string): Promise<ValidationResult>;
@@ -0,0 +1,135 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { delimiter, join, resolve } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ export function supportedNode(version = process.versions.node) {
7
+ const [major = 0, minor = 0] = version.replace(/^v/, "").split(".").map(Number);
8
+ return major > 22 || (major === 22 && minor >= 13);
9
+ }
10
+ export async function findSdk(dir) {
11
+ const filename = process.platform === "win32" ? "agent-sdk.cmd" : "agent-sdk";
12
+ const candidates = [join(resolve(dir), "node_modules", ".bin", filename),
13
+ ...((process.env.PATH ?? "").split(delimiter).filter(Boolean).map(p => join(p, filename)))];
14
+ for (const candidate of candidates) {
15
+ try {
16
+ await access(candidate, constants.X_OK);
17
+ return candidate;
18
+ }
19
+ catch { /* next installed CLI */ }
20
+ }
21
+ }
22
+ // Resolve only the converter's known dependencies. Resolution never imports or executes them.
23
+ // Hoisted dependencies are valid; integration tests must use an independent directory.
24
+ async function missingDependencies(dir) {
25
+ const manifestPath = join(resolve(dir), "package.json");
26
+ let source;
27
+ try {
28
+ source = await readFile(manifestPath, "utf8");
29
+ }
30
+ catch (error) {
31
+ if (error.code === "ENOENT")
32
+ return [];
33
+ throw error;
34
+ }
35
+ const manifest = JSON.parse(source);
36
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
37
+ throw new Error("Invalid package.json");
38
+ const dependencies = manifest.dependencies;
39
+ if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies))
40
+ return [];
41
+ const require = createRequire(manifestPath);
42
+ return ["@grok/agent-sdk", "zod"].filter(name => {
43
+ if (!Object.hasOwn(dependencies, name))
44
+ return false;
45
+ try {
46
+ require.resolve(name);
47
+ return false;
48
+ }
49
+ catch (error) {
50
+ if (error.code === "MODULE_NOT_FOUND")
51
+ return true;
52
+ throw error; // Broken exports/configuration are validation failures, not missing installs.
53
+ }
54
+ });
55
+ }
56
+ export function validationMessage(result, lang) {
57
+ if (lang !== "zh" || result.status !== "dependencies-missing" || !result.missingDependencies?.length)
58
+ return result.message;
59
+ return `SDK 自检尚未执行:缺少项目依赖 ${result.missingDependencies.join(", ")}。请先在项目目录运行 npm install,再运行 agent2cursor doctor --dir .。`;
60
+ }
61
+ // Never relay SDK stdout/stderr: source modules and runtime errors may contain credentials.
62
+ // Only diagnostic counts and a closed set of status fields cross the process boundary.
63
+ export async function validateProject(dir, sdk) {
64
+ const command = `agent-sdk validate --dir ${JSON.stringify(resolve(dir))}`;
65
+ let missing;
66
+ try {
67
+ missing = await missingDependencies(dir);
68
+ }
69
+ catch {
70
+ return { status: "failed", command, message: "Could not resolve project dependencies. Check package.json and installed package configuration; raw details withheld." };
71
+ }
72
+ if (missing.length)
73
+ return { status: "dependencies-missing", command, missingDependencies: missing,
74
+ message: `SDK validation has not run: missing project dependencies ${missing.join(", ")}. Run npm install in the project, then \`agent2cursor doctor --dir .\`.` };
75
+ const executable = sdk ?? await findSdk(dir);
76
+ if (!executable)
77
+ return { status: "unavailable", command,
78
+ message: "Agent SDK CLI is not installed. Run npm install in the generated project, then agent2cursor doctor --dir <project>." };
79
+ return new Promise(resolveResult => {
80
+ let stdout = "";
81
+ let overflow = false;
82
+ let timedOut = false;
83
+ const child = spawn(executable, ["validate", "--dir", resolve(dir), "--json"], {
84
+ stdio: ["ignore", "pipe", "ignore"],
85
+ env: { ...process.env, AGENT_SERVE_NO_UPDATE_CHECK: "1", NO_UPDATE_NOTIFIER: "1" },
86
+ shell: false,
87
+ });
88
+ const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 60_000);
89
+ child.stdout.on("data", (chunk) => {
90
+ if (stdout.length + chunk.length > 2 * 1024 * 1024) {
91
+ overflow = true;
92
+ child.kill("SIGKILL");
93
+ }
94
+ else
95
+ stdout += chunk.toString("utf8");
96
+ });
97
+ child.on("error", () => { clearTimeout(timer); resolveResult({ status: "failed", command, message: "Could not start Agent SDK CLI." }); });
98
+ child.on("close", code => {
99
+ clearTimeout(timer);
100
+ if (timedOut || overflow)
101
+ return resolveResult({ status: "failed", command,
102
+ message: timedOut ? "Validation exceeded 60 seconds." : "Validation output exceeded the safe limit." });
103
+ let parsed;
104
+ try {
105
+ parsed = JSON.parse(stdout);
106
+ }
107
+ catch { /* no raw output fallback */ }
108
+ let errors = 0, warnings = 0;
109
+ const count = (value) => {
110
+ if (!value || typeof value !== "object")
111
+ return;
112
+ if (Array.isArray(value)) {
113
+ value.forEach(count);
114
+ return;
115
+ }
116
+ const record = value;
117
+ if (record.severity === "error")
118
+ errors++;
119
+ if (record.severity === "warning")
120
+ warnings++;
121
+ Object.values(record).forEach(count);
122
+ };
123
+ count(parsed);
124
+ const document = parsed && typeof parsed === "object" && !Array.isArray(parsed)
125
+ ? parsed : undefined;
126
+ const sdkShape = document?.ok === true && Array.isArray(document.agents)
127
+ && document.agents.length > 0 && document.agents.every(agent => agent && typeof agent === "object"
128
+ && Array.isArray(agent.diagnostics));
129
+ const status = code === 0 && sdkShape && errors === 0 ? "passed" : "failed";
130
+ resolveResult({ status, command, message: status === "passed"
131
+ ? `SDK validation passed: ${errors} errors, ${warnings} warnings. This does not prove live model or deployment behavior.`
132
+ : `SDK validation failed (exit ${code ?? "signal"}; ${errors} errors, ${warnings} warnings). Raw diagnostics withheld. Check installed project dependencies and run the SDK locally for details.` });
133
+ });
134
+ });
135
+ }
@@ -0,0 +1,19 @@
1
+ interface FileIdentity {
2
+ dev: bigint;
3
+ ino: bigint;
4
+ birthtimeNs: bigint;
5
+ }
6
+ interface ReportIdentity extends FileIdentity {
7
+ size: bigint;
8
+ mtimeNs: bigint;
9
+ ctimeNs: bigint;
10
+ }
11
+ export interface WrittenProject {
12
+ readonly directory: string;
13
+ readonly directories: ReadonlyMap<string, FileIdentity>;
14
+ readonly reports: ReadonlyMap<string, ReportIdentity>;
15
+ }
16
+ export declare function writeProject(out: string, files: Map<string, string | Buffer>): Promise<WrittenProject>;
17
+ /** Update only the report created by this conversion, never a replacement reached by a path. */
18
+ export declare function updateProjectReport(project: WrittenProject, name: string, contents: string): Promise<void>;
19
+ export {};
package/dist/write.js ADDED
@@ -0,0 +1,134 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, mkdir, mkdtemp, open, realpath, rename, rm, rmdir } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
4
+ function identity(stat) {
5
+ return { dev: stat.dev, ino: stat.ino, birthtimeNs: stat.birthtimeNs };
6
+ }
7
+ function sameIdentity(stat, expected) {
8
+ return stat.dev === expected.dev && stat.ino === expected.ino && stat.birthtimeNs === expected.birthtimeNs;
9
+ }
10
+ async function safeParents(path) {
11
+ const root = parse(path).root;
12
+ let current = root;
13
+ const directories = new Map([[root, identity(await lstat(root, { bigint: true }))]]);
14
+ for (const segment of relative(root, path).split(/[\\/]/).filter(Boolean)) {
15
+ current = join(current, segment);
16
+ try {
17
+ const stat = await lstat(current);
18
+ if (stat.isSymbolicLink() || !stat.isDirectory())
19
+ throw new Error("Output ancestors must be real directories, not symlinks.");
20
+ }
21
+ catch (error) {
22
+ if (error.code !== "ENOENT")
23
+ throw error;
24
+ try {
25
+ await mkdir(current);
26
+ }
27
+ catch (creationError) {
28
+ if (creationError.code !== "EEXIST")
29
+ throw creationError;
30
+ const stat = await lstat(current);
31
+ if (!stat.isDirectory() || stat.isSymbolicLink())
32
+ throw new Error("Output parent changed during creation.");
33
+ }
34
+ }
35
+ const stat = await lstat(current, { bigint: true });
36
+ if (stat.isSymbolicLink() || !stat.isDirectory())
37
+ throw new Error("Output parent changed during creation.");
38
+ directories.set(current, identity(stat));
39
+ }
40
+ return directories;
41
+ }
42
+ export async function writeProject(out, files) {
43
+ let destination = resolve(out);
44
+ // macOS system aliases are platform-owned; arbitrary user symlinks still fail.
45
+ if (process.platform === "darwin") {
46
+ for (const alias of ["/var", "/tmp"]) {
47
+ if (destination === alias || destination.startsWith(`${alias}/`)) {
48
+ destination = join(await realpath(alias), relative(alias, destination));
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ for (const name of files.keys()) {
54
+ if (!name || isAbsolute(name) || name.includes("\\") || name.split("/").some(p => p === ".." || p === "." || !p)) {
55
+ throw new Error("Unsafe generated output path.");
56
+ }
57
+ }
58
+ const directories = await safeParents(dirname(destination));
59
+ try {
60
+ await lstat(destination);
61
+ throw new Error(`Output already exists; choose a new directory: ${destination}`);
62
+ }
63
+ catch (error) {
64
+ if (error.code !== "ENOENT")
65
+ throw error;
66
+ }
67
+ const staging = await mkdtemp(join(dirname(destination), ".agent2cursor-"));
68
+ directories.set(destination, identity(await lstat(staging, { bigint: true })));
69
+ const reports = new Map();
70
+ let reserved = false;
71
+ try {
72
+ for (const [name, contents] of files) {
73
+ const target = join(staging, name);
74
+ await mkdir(dirname(target), { recursive: true });
75
+ const handle = await open(target, "wx");
76
+ try {
77
+ await handle.writeFile(contents);
78
+ if (name === "MIGRATION.zh-CN.md" || name === "MIGRATION.en.md") {
79
+ const stat = await handle.stat({ bigint: true });
80
+ reports.set(name, { ...identity(stat), size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs });
81
+ }
82
+ }
83
+ finally {
84
+ await handle.close();
85
+ }
86
+ }
87
+ // Reserve the destination exclusively before publishing the completed tree.
88
+ await mkdir(destination);
89
+ reserved = true;
90
+ await rename(staging, destination);
91
+ reserved = false;
92
+ return { directory: destination, directories, reports };
93
+ }
94
+ finally {
95
+ await rm(staging, { recursive: true, force: true });
96
+ if (reserved)
97
+ await rmdir(destination);
98
+ }
99
+ }
100
+ /** Update only the report created by this conversion, never a replacement reached by a path. */
101
+ export async function updateProjectReport(project, name, contents) {
102
+ const expected = project.reports.get(name);
103
+ if (!expected)
104
+ throw new Error("Report was not created by this conversion.");
105
+ const verifyDirectories = async () => {
106
+ for (const [path, expectedDirectory] of project.directories) {
107
+ const stat = await lstat(path, { bigint: true });
108
+ if (!stat.isDirectory() || stat.isSymbolicLink() || !sameIdentity(stat, expectedDirectory)) {
109
+ throw new Error("Output directory changed before report update; refusing to write.");
110
+ }
111
+ }
112
+ };
113
+ await verifyDirectories();
114
+ const target = join(project.directory, name);
115
+ // Do not truncate while resolving the path. Even if an ancestor changes after
116
+ // verification, fstat must identify our original regular file before any write.
117
+ const handle = await open(target, constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
118
+ try {
119
+ const stat = await handle.stat({ bigint: true });
120
+ const entry = await lstat(target, { bigint: true });
121
+ if (!stat.isFile() || stat.nlink !== 1n || !sameIdentity(stat, expected)
122
+ || stat.size !== expected.size || stat.mtimeNs !== expected.mtimeNs || stat.ctimeNs !== expected.ctimeNs
123
+ || entry.isSymbolicLink() || !sameIdentity(entry, expected)) {
124
+ throw new Error("Generated report changed before update; refusing to write.");
125
+ }
126
+ await verifyDirectories();
127
+ // The verified descriptor stays attached to our file even if paths move now.
128
+ await handle.writeFile(contents, "utf8");
129
+ await handle.truncate(Buffer.byteLength(contents, "utf8"));
130
+ }
131
+ finally {
132
+ await handle.close();
133
+ }
134
+ }
@@ -0,0 +1,86 @@
1
+ # agent2cursor 验收记录
2
+
3
+ 更新日期:2026-10-28。版本:`0.1.0`。目标 SDK:`@grok/agent-sdk@0.3.0`。用户已确定项目名为 `agent2cursor`。
4
+
5
+ 当前用户决定(2026-10-28):GitHub 仓库 `mreasonyang/agent2cursor` 公开,并授权将 `@kyun.si/agent2cursor@0.1.0` 发布到 npm。根 package.json 保持 `private: true`,单独构建公开消费者归档;安装后的命令仍为 `agent2cursor`。npm 发布结果单独验证。
6
+
7
+ ## agent2cursor 更名(2026-10-28)
8
+
9
+ 项目由 `cc-to-cursor-agent` 更名为 `agent2cursor`,覆盖包名与锁文件、CLI、生成源码及报告、两个 demo 与生成模板、测试、双语文档、GitHub 仓库链接,以及 README 动图、海报和可编辑源文件。版本保持 `0.1.0`。
10
+
11
+ 新命令为 `agent2cursor`;示例快照路径为 `.agent2cursor/pr-review.json`,禁用存根错误码为 `AGENT2CURSOR_STUB_DISABLED`,可选合并开关为 `AGENT2CURSOR_ALLOW_MERGE`。旧命令不安装别名,旧开关不作为回退读取;合并仍默认关闭。
12
+
13
+ 更名前的独立评测原文、`docs/evidence/` 中既有 JSON 和固定 tstack 上游快照保留原始名称、路径与哈希;本次检查另行记录,不把历史真实模型结果算作更名后重跑。
14
+
15
+ 本次 `npm run check` 通过 74 项测试(含工作区原有的 Codex 引用 skill 不加载测试)。`npm run test:sdk` 的 Claude、Codex、tstack、full-send、pr-review 五类工程通过独立安装前后检查、SDK 结构校验、严格 TypeScript 和 eval 发现,证据位于 `.validation/acceptance-XieMEy/evidence.json`。本轮未执行真实模型或部署。
16
+
17
+ `npm pack` 产物为 `agent2cursor-0.1.0.tgz`,在仓库外的干净 consumer 安装后验证了真实 `agent2cursor` bin 的帮助、版本、混合 Claude/Codex/skill 转换、两个 demo 和缺依赖时的 doctor 状态;运行源码与输出不再含旧项目标识。README 动图重新渲染为 960×540、10 fps、14 秒(140 帧),并核对海报画面和 [SOURCE.json](demo-video/SOURCE.json) 的新哈希。GitHub 仓库与 origin、本地目录均已更名,更名当时仍为私有;该轮没有发布到 npm。
18
+
19
+ ## 已验证
20
+
21
+ | 项目 | 结果 | 证据边界 |
22
+ | --- | --- | --- |
23
+ | TypeScript 构建 | PASS | 本次本机 Node 26.11.0 与生成项目严格类型检查通过;历史私有 CI Node 22.13 / 24 已通过,当前提交 CI 见工作流 |
24
+ | 自动测试 | PASS,73 项 | 2026-10-28 重跑 npm run check;新增 Codex TOML、混合输入、逐字段元数据、权限提示、敏感字段拒绝、解析错误不回显及打包前 CLI 回归 |
25
+ | Codex 转换 | PASS,2 个子代理、1 个 skill | 合成 TOML 与 openai.yaml 样例;配置发现、类型检查与 SDK 结构校验通过,未跑真实模型 |
26
+ | 四类 Claude 示例 | PASS,4 个子代理 | 独立编写的官方示例模式 fixture;不是官方示例逐字副本 |
27
+ | 官方原始示例 | PASS,4 个子代理,零诊断 | 从官方 Markdown 下载并提取 code-reviewer/debugger/data-scientist/db-reader,在忽略目录转换;不把官方原文作为 fixture 再分发,哈希见 official-examples.provenance.json |
28
+ | tstack 全量转换 | PASS | 9 个 skill,1 个 pr_wait agent tool;SDK info 核对数量和 execution 字段 |
29
+ | 依赖闭包 | PASS | 单独 full-send 输入生成 5 个 skill,含 4 个依赖 |
30
+ | SDK 结构验证 | PASS | 5 类生成项目均零 error;full-send demo 有 1 条预期禁用脚本 warning |
31
+ | Eval 发现 | PASS | Claude 4 条、Codex 3 条、tstack 9 条、full-send 6 条、pr-review 3 条;只表示发现,不表示模型执行通过 |
32
+ | 干净 tarball 安装(既有验收) | PASS | npm pack 后在独立 consumer 安装;真实 bin 的 convert、demo、doctor 均通过 SDK 自检 |
33
+ | 许可 | PASS | 固定 tstack 提交、逐文件哈希、完整 MIT;普通 tstack 转换也保留 LICENSE-tstack |
34
+
35
+ 可重现命令:
36
+
37
+ ```sh
38
+ npm ci --ignore-scripts
39
+ npm run check
40
+ npm install --prefix .validation --ignore-scripts --no-audit --no-fund @grok/agent-sdk@0.3.0
41
+ npm run test:sdk
42
+ ```
43
+
44
+ 本机最终结构和类型检查证据在忽略目录 `.validation/acceptance-JjGsx6/evidence.json`;生成工程位于仓库外的系统临时目录,分别安装自身依赖,不再借用 `.validation` 的依赖。不包含凭证或真实模型轨迹。官方原始示例的历史产物保留在旧验收目录下的 `.validation/official-converted/`。首轮 [GitHub CI](https://github.com/mreasonyang/agent2cursor/actions/runs/40473284090) 已通过;最终提交状态以 [check 工作流](https://github.com/mreasonyang/agent2cursor/actions/workflows/check.yml) 为准。
45
+
46
+ ## Codex 适配验收(2026-10-28)
47
+
48
+ 支持独立 `.codex/agents/*.toml`、显式选择的 TOML 文件、Codex 通用 skill 及 `agents/openai.yaml`;输出仍是 Cursor Agent SDK。旧式角色注册和父会话配置不在本次转换范围。映射边界见 [上游核实](upstream-verification.md#codex-input-adapter-2026-10-28),本次测试结果及源码哈希见 [Codex 证据](evidence/codex-adapter.json)。
49
+
50
+ 73 项本地测试和 5 类 SDK 工程集成检查通过;Codex 的 3 条路由 eval 仅完成发现,没有执行真实模型。此前 Claude/tstack 的在线运行记录不能作为 Codex 运行行为等价的证据。
51
+
52
+ ## 独立评测问题修复
53
+
54
+ 复测基于 `evaluation/20261027-1822/给作者.md` 指出的用例,修复前基线为 `e63b662`。脱敏结果和源码哈希见 [回归证据](evidence/evaluation-fixes.json)。
55
+
56
+ - `N-SK-05` / `B-12`:默认转换退出 0,状态为 `dependencies-missing`,列出 SDK / zod 缺失,未声称通过校验。安装各自依赖后 SDK validate 均为零诊断。doctor 在依赖缺失时仍退出 1,真实语法错误仍被拒绝。
57
+ - `N-MX-02` / `B-07`:保留原文及已有提示;追加提示时,正文结论明确降级。inspect 与 convert 使用相同的最终映射结论,不保留矛盾的“原样迁移”记录。
58
+ - `N-AG-05` / `B-12`:报告分别列出 WebFetch 等工具名引用,源正文逐字保留。新增 WebSearch 覆盖与标识符边界测试,说明扫描采用固定名单。
59
+ - `N-AG-04`:权限损失显示在终端摘要第一行和报告开头,JSON 摘要也包含 permissionWarning;未向子代理配置伪造 tools / disallowedTools / permissionMode。
60
+ - `B-11` 继续拒绝敏感文件;输入脚本执行哨兵仍未出现。58 项单测通过;四类生成工程在独立目录中通过依赖安装前后、SDK 结构、严格类型和 eval 发现检查。
61
+
62
+ 此次修复不改变历史真实模型和线上验收的证据范围。
63
+
64
+ ## 线上验收
65
+
66
+ 修复后的独立评测见 [最终报告](../evaluation/20261027-2216/复测结论.md) 和 [汇总证据](evidence/independent-evaluation.json):正常转换与安装后校验均为 24/24;3 个真实任务各重复 3 次,与直接复制/插件对照结果一致。生成的路由 eval 初次为 35/41,修改提示词与超时后为 41/41。汇总记录修复源码的哈希,不把旧提交的结果与新提示词混为一轮。
67
+
68
+ 以下是较早的 demo 验收。重复的 `deployments/pr-review` 本地验收副本已移除,维护入口统一为 `demos/pr-review`;原始证据里的历史路径和哈希仍可在下述历史提交中查验。
69
+
70
+ 以下结果来自更名前的 `cc2agent`。原始 [JSON 证据](evidence/online-2026-10-27.json) 中的仓库名和源码哈希保留不变;对应源码可在更名前提交 `f9fc7fe682252b82bce904f6df0d3da0f3d62de5` 中查验。本次更名重跑本地构建、测试、SDK 结构校验和打包验证,不把历史线上结果算作更名后重新运行。
71
+
72
+ 已通过官方远程浏览器登录流程完成 Cursor SDK 认证;用户不需要在运行电脑旁操作。认证信息仅由 SDK 管理,未写入代码或证据。
73
+
74
+ - 真实模型 loadedSkill eval:PASS,转换生成的 migration-smoke,1 case / 2 assertions。
75
+ - full-send 安全触发回合:PASS,配置私有仓库和默认未配置仓库各一次真实 `run`,均成功加载 full-send;每次 1 tool call、0 写入/执行调用、未调用 merge_pr。该场景仅检查 skill 加载与用途说明,不代表完整自动送 PR 流程。
76
+ - 三视角 Council eval:PASS,1 case / 9 assertions(成功、三次完成委派、Council review 标题、降级声明、三行评分表);默认同模型,不能声称三厂商或完整隔离已验证。
77
+ - 测试 PR 事件:PASS,[私有验收 PR #1](https://github.com/mreasonyang/agent2cursor/pull/1) 经 HMAC 签名回放进入本地会话并完成。服务端读取只读快照,模型在 sandbox 内完成三次委派及中文评分表:8 次工具调用、0 工具错误、0 写入/执行调用。GitHub 评论、reaction、review 均为零。测试 PR 已关闭且未合并。回放不是 GitHub 实际 webhook 投递或托管验收。
78
+ - 托管部署与 URL:BLOCKED;SDK `deploy --dry-run` 明确返回账号没有默认团队。需要用户提供已开通 hosting 的团队 ID;未执行实际部署,没有 URL。
79
+ - GitHub CI:已通过 Node 22.13 / 24 单测和 npm pack,以及 SDK 兼容性任务。
80
+ - 该轮历史验收未执行 npm / 公开 GitHub / 社交发布;本次新增的 GitHub 公开与 npm 发布授权单独执行和验收。
81
+
82
+ ## 不能混同的边界
83
+
84
+ 脚本存根明确禁用,不能以 validate 成功声称原脚本可执行。工具名扫描不自动改写任意输入 prompt。SDK 子代理权限与根白名单不等价;多代理隔离须独立验证。full-send 默认准备本地修改和 PR 草稿,自动测试、push、建 PR 尚未实现;合并 server tool 默认关闭并有审批和 head SHA 门禁。本地 sandbox 可能阻止 MCP server tool;pr-review 使用经过 SHA 校验的服务端快照解决本场景,不关闭 sandbox,也不声称 pr_wait 在 sandbox 模型回合中已经通过。
85
+
86
+ 两个 demo 显式关闭 SDK 的自动回帖、reaction 和 mention 触发。未配置仓库时禁用 GitHub channel。上述运行证据只覆盖具体用例,不能推广为所有输入下均无写入副作用。脱敏结果及源文件哈希见 [线上验收证据](evidence/online-2026-10-27.json)。