@agentoryhq/adapter-cursor 0.1.1
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/dist/align.d.ts +7 -0
- package/dist/align.js +64 -0
- package/dist/asset-index.d.ts +12 -0
- package/dist/asset-index.js +278 -0
- package/dist/discover.d.ts +4 -0
- package/dist/discover.js +87 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +29 -0
- package/dist/transcript.d.ts +29 -0
- package/dist/transcript.js +139 -0
- package/package.json +32 -0
package/dist/align.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ApplyAlignOpts, ApplyAlignResult } from "@agentoryhq/runtime-contract";
|
|
2
|
+
/**
|
|
3
|
+
* Write confirmed align patch to Cursor project files.
|
|
4
|
+
* Snapshots prior mcp.json under `.cursor/agentory-snapshots/`.
|
|
5
|
+
* All-or-nothing: on failure restores snapshot when possible.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyAlignPatch(opts: ApplyAlignOpts): ApplyAlignResult;
|
package/dist/align.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Write confirmed align patch to Cursor project files.
|
|
5
|
+
* Snapshots prior mcp.json under `.cursor/agentory-snapshots/`.
|
|
6
|
+
* All-or-nothing: on failure restores snapshot when possible.
|
|
7
|
+
*/
|
|
8
|
+
export function applyAlignPatch(opts) {
|
|
9
|
+
const mcpPath = path.join(opts.projectRoot, ".cursor", "mcp.json");
|
|
10
|
+
const skillsPath = path.join(opts.projectRoot, ".cursor", "agentory-skills.json");
|
|
11
|
+
if (!opts.confirmed) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
error: "refusing write: align not confirmed",
|
|
15
|
+
mcpPath,
|
|
16
|
+
skillsPath,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const cursorDir = path.join(opts.projectRoot, ".cursor");
|
|
20
|
+
fs.mkdirSync(cursorDir, { recursive: true });
|
|
21
|
+
let snapshotPath;
|
|
22
|
+
let priorMcp;
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(mcpPath)) {
|
|
25
|
+
priorMcp = fs.readFileSync(mcpPath, "utf8");
|
|
26
|
+
const snapDir = path.join(cursorDir, "agentory-snapshots");
|
|
27
|
+
fs.mkdirSync(snapDir, { recursive: true });
|
|
28
|
+
snapshotPath = path.join(snapDir, `mcp-${Date.now()}.json`);
|
|
29
|
+
fs.writeFileSync(snapshotPath, priorMcp, "utf8");
|
|
30
|
+
}
|
|
31
|
+
const mcpServers = {};
|
|
32
|
+
for (const item of opts.patch.mcp) {
|
|
33
|
+
const command = item.command?.trim() || "echo";
|
|
34
|
+
const parts = command.split(/\s+/);
|
|
35
|
+
mcpServers[item.name] = {
|
|
36
|
+
command: parts[0] ?? "echo",
|
|
37
|
+
args: parts.length > 1 ? parts.slice(1) : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const mcpBody = JSON.stringify({ mcpServers }, null, 2);
|
|
41
|
+
const skillsBody = JSON.stringify({ skills: opts.patch.skills }, null, 2);
|
|
42
|
+
fs.writeFileSync(mcpPath, `${mcpBody}\n`, "utf8");
|
|
43
|
+
try {
|
|
44
|
+
fs.writeFileSync(skillsPath, `${skillsBody}\n`, "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (priorMcp !== undefined)
|
|
48
|
+
fs.writeFileSync(mcpPath, priorMcp, "utf8");
|
|
49
|
+
else if (fs.existsSync(mcpPath))
|
|
50
|
+
fs.unlinkSync(mcpPath);
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, mcpPath, skillsPath, snapshotPath };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: err instanceof Error ? err.message : String(err),
|
|
59
|
+
mcpPath,
|
|
60
|
+
skillsPath,
|
|
61
|
+
snapshotPath,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AdapterContext, AssetIndexResult } from "@agentoryhq/runtime-contract";
|
|
2
|
+
/** Map Cursor project dir slug → absolute folder path (best-effort). */
|
|
3
|
+
export declare function buildCursorProjectPathMap(homeDir: string): Map<string, string>;
|
|
4
|
+
/** Read only enough of the transcript to derive a short title — never return full body. */
|
|
5
|
+
export declare function titleFromTranscriptJsonl(file: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Cursor asset index (metadata only) — no Host fixtures.
|
|
8
|
+
* - Legacy: `.cursor/sessions/*.meta.json`
|
|
9
|
+
* - Native: ~/.cursor/projects/<slug>/agent-transcripts/<id>/<id>.jsonl
|
|
10
|
+
* - Rules / memory dirs (path + mtime only)
|
|
11
|
+
*/
|
|
12
|
+
export declare function indexCursorSessions(ctx: AdapterContext): AssetIndexResult;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const TITLE_MAX = 80;
|
|
4
|
+
function readJsonFile(file) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function mtimeIso(file) {
|
|
13
|
+
try {
|
|
14
|
+
return fs.statSync(file).mtime.toISOString();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return new Date(0).toISOString();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function slugifyAbsPath(abs) {
|
|
21
|
+
return abs.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\//g, "-");
|
|
22
|
+
}
|
|
23
|
+
/** Map Cursor project dir slug → absolute folder path (best-effort). */
|
|
24
|
+
export function buildCursorProjectPathMap(homeDir) {
|
|
25
|
+
const map = new Map();
|
|
26
|
+
const add = (folderUri) => {
|
|
27
|
+
const raw = folderUri.startsWith("file://") ? folderUri.slice("file://".length) : folderUri;
|
|
28
|
+
if (!raw.startsWith("/"))
|
|
29
|
+
return;
|
|
30
|
+
map.set(slugifyAbsPath(raw), raw);
|
|
31
|
+
};
|
|
32
|
+
const storageJson = path.join(homeDir, "Library", "Application Support", "Cursor", "User", "globalStorage", "storage.json");
|
|
33
|
+
const storage = readJsonFile(storageJson);
|
|
34
|
+
if (storage && typeof storage === "object") {
|
|
35
|
+
const folders = storage
|
|
36
|
+
.backupWorkspaces?.folders;
|
|
37
|
+
if (Array.isArray(folders)) {
|
|
38
|
+
for (const f of folders) {
|
|
39
|
+
if (f?.folderUri)
|
|
40
|
+
add(f.folderUri);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const wsRoot = path.join(homeDir, "Library", "Application Support", "Cursor", "User", "workspaceStorage");
|
|
45
|
+
try {
|
|
46
|
+
for (const name of fs.readdirSync(wsRoot)) {
|
|
47
|
+
const wj = readJsonFile(path.join(wsRoot, name, "workspace.json"));
|
|
48
|
+
if (wj && typeof wj === "object") {
|
|
49
|
+
const folder = wj.folder;
|
|
50
|
+
if (typeof folder === "string")
|
|
51
|
+
add(folder);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* no workspaceStorage */
|
|
57
|
+
}
|
|
58
|
+
return map;
|
|
59
|
+
}
|
|
60
|
+
function extractTextBlob(content) {
|
|
61
|
+
if (typeof content === "string")
|
|
62
|
+
return content;
|
|
63
|
+
if (Array.isArray(content)) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
for (const c of content) {
|
|
66
|
+
if (typeof c === "string")
|
|
67
|
+
parts.push(c);
|
|
68
|
+
else if (c && typeof c === "object" && typeof c.text === "string") {
|
|
69
|
+
parts.push(c.text);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return parts.join(" ");
|
|
73
|
+
}
|
|
74
|
+
if (content && typeof content === "object" && typeof content.text === "string") {
|
|
75
|
+
return content.text;
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
/** Read only enough of the transcript to derive a short title — never return full body. */
|
|
80
|
+
export function titleFromTranscriptJsonl(file) {
|
|
81
|
+
try {
|
|
82
|
+
const fd = fs.openSync(file, "r");
|
|
83
|
+
try {
|
|
84
|
+
const buf = Buffer.alloc(16 * 1024);
|
|
85
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
86
|
+
const chunk = buf.slice(0, n).toString("utf8");
|
|
87
|
+
for (const line of chunk.split("\n")) {
|
|
88
|
+
if (!line.trim())
|
|
89
|
+
continue;
|
|
90
|
+
let o;
|
|
91
|
+
try {
|
|
92
|
+
o = JSON.parse(line);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!o || typeof o !== "object")
|
|
98
|
+
continue;
|
|
99
|
+
const row = o;
|
|
100
|
+
if (row.role !== "user")
|
|
101
|
+
continue;
|
|
102
|
+
const msg = row.message;
|
|
103
|
+
let text = "";
|
|
104
|
+
if (typeof msg === "string")
|
|
105
|
+
text = msg;
|
|
106
|
+
else if (msg && typeof msg === "object") {
|
|
107
|
+
text = extractTextBlob(msg.content);
|
|
108
|
+
}
|
|
109
|
+
text = text
|
|
110
|
+
.replace(/<timestamp>[\s\S]*?<\/timestamp>/gi, " ")
|
|
111
|
+
.replace(/<\/?user_query>/gi, " ")
|
|
112
|
+
.replace(/\s+/g, " ")
|
|
113
|
+
.trim();
|
|
114
|
+
if (!text)
|
|
115
|
+
continue;
|
|
116
|
+
return text.length > TITLE_MAX ? `${text.slice(0, TITLE_MAX)}…` : text;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
fs.closeSync(fd);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
function collectMetaSessions(cwd, home) {
|
|
129
|
+
const sessions = [];
|
|
130
|
+
const sessionDirs = [path.join(cwd, ".cursor", "sessions"), path.join(home, ".cursor", "sessions")];
|
|
131
|
+
for (const dir of sessionDirs) {
|
|
132
|
+
if (!fs.existsSync(dir))
|
|
133
|
+
continue;
|
|
134
|
+
for (const name of fs.readdirSync(dir)) {
|
|
135
|
+
if (!name.endsWith(".meta.json"))
|
|
136
|
+
continue;
|
|
137
|
+
const file = path.join(dir, name);
|
|
138
|
+
const raw = readJsonFile(file);
|
|
139
|
+
if (!raw || typeof raw !== "object")
|
|
140
|
+
continue;
|
|
141
|
+
const o = raw;
|
|
142
|
+
const displayName = typeof o.displayName === "string" && o.displayName.trim()
|
|
143
|
+
? o.displayName.trim()
|
|
144
|
+
: name.replace(/\.meta\.json$/, "");
|
|
145
|
+
const updatedAt = typeof o.updatedAt === "string" ? o.updatedAt : mtimeIso(file);
|
|
146
|
+
const projectPath = typeof o.projectPath === "string" ? o.projectPath : cwd;
|
|
147
|
+
sessions.push({
|
|
148
|
+
sourceKey: `cursor-session:${file}`,
|
|
149
|
+
displayName,
|
|
150
|
+
projectPath,
|
|
151
|
+
updatedAt,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return sessions;
|
|
156
|
+
}
|
|
157
|
+
function collectNativeTranscripts(home) {
|
|
158
|
+
const sessions = [];
|
|
159
|
+
const projectsRoot = path.join(home, ".cursor", "projects");
|
|
160
|
+
if (!fs.existsSync(projectsRoot))
|
|
161
|
+
return sessions;
|
|
162
|
+
const pathMap = buildCursorProjectPathMap(home);
|
|
163
|
+
let projectNames = [];
|
|
164
|
+
try {
|
|
165
|
+
projectNames = fs.readdirSync(projectsRoot);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return sessions;
|
|
169
|
+
}
|
|
170
|
+
for (const slug of projectNames) {
|
|
171
|
+
const transcriptsRoot = path.join(projectsRoot, slug, "agent-transcripts");
|
|
172
|
+
if (!fs.existsSync(transcriptsRoot))
|
|
173
|
+
continue;
|
|
174
|
+
const projectPath = pathMap.get(slug) || slug;
|
|
175
|
+
let ids = [];
|
|
176
|
+
try {
|
|
177
|
+
ids = fs.readdirSync(transcriptsRoot);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
for (const id of ids) {
|
|
183
|
+
const file = path.join(transcriptsRoot, id, `${id}.jsonl`);
|
|
184
|
+
if (!fs.existsSync(file))
|
|
185
|
+
continue;
|
|
186
|
+
try {
|
|
187
|
+
if (!fs.statSync(file).isFile())
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const title = titleFromTranscriptJsonl(file);
|
|
194
|
+
sessions.push({
|
|
195
|
+
sourceKey: `cursor-transcript:${file}`,
|
|
196
|
+
displayName: title || id.slice(0, 8),
|
|
197
|
+
projectPath,
|
|
198
|
+
updatedAt: mtimeIso(file),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return sessions;
|
|
203
|
+
}
|
|
204
|
+
function collectRulesAndMemory(cwd, home) {
|
|
205
|
+
const memories = [];
|
|
206
|
+
const ruleDirs = [path.join(cwd, ".cursor", "rules"), path.join(home, ".cursor", "rules")];
|
|
207
|
+
for (const dir of ruleDirs) {
|
|
208
|
+
if (!fs.existsSync(dir))
|
|
209
|
+
continue;
|
|
210
|
+
for (const name of fs.readdirSync(dir)) {
|
|
211
|
+
const file = path.join(dir, name);
|
|
212
|
+
try {
|
|
213
|
+
if (!fs.statSync(file).isFile())
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
memories.push({
|
|
220
|
+
sourceKey: `cursor-rules:${file}`,
|
|
221
|
+
kind: "rules",
|
|
222
|
+
pathLabel: path.relative(cwd, file).replace(/\\/g, "/") || name,
|
|
223
|
+
projectPath: cwd,
|
|
224
|
+
updatedAt: mtimeIso(file),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (memories.some((m) => m.kind === "rules"))
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
const memoryDirs = [path.join(cwd, ".cursor", "memory"), path.join(home, ".cursor", "memory")];
|
|
231
|
+
for (const dir of memoryDirs) {
|
|
232
|
+
if (!fs.existsSync(dir))
|
|
233
|
+
continue;
|
|
234
|
+
for (const name of fs.readdirSync(dir)) {
|
|
235
|
+
const file = path.join(dir, name);
|
|
236
|
+
try {
|
|
237
|
+
if (!fs.statSync(file).isFile())
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
memories.push({
|
|
244
|
+
sourceKey: `cursor-memory:${file}`,
|
|
245
|
+
kind: "memory",
|
|
246
|
+
pathLabel: path.relative(cwd, file).replace(/\\/g, "/") || name,
|
|
247
|
+
projectPath: cwd,
|
|
248
|
+
updatedAt: mtimeIso(file),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (memories.some((m) => m.kind === "memory"))
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
return memories;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Cursor asset index (metadata only) — no Host fixtures.
|
|
258
|
+
* - Legacy: `.cursor/sessions/*.meta.json`
|
|
259
|
+
* - Native: ~/.cursor/projects/<slug>/agent-transcripts/<id>/<id>.jsonl
|
|
260
|
+
* - Rules / memory dirs (path + mtime only)
|
|
261
|
+
*/
|
|
262
|
+
export function indexCursorSessions(ctx) {
|
|
263
|
+
const { cwd, homeDir: home } = ctx;
|
|
264
|
+
const sessions = [];
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
const pushSession = (s) => {
|
|
267
|
+
if (seen.has(s.sourceKey))
|
|
268
|
+
return;
|
|
269
|
+
seen.add(s.sourceKey);
|
|
270
|
+
sessions.push(s);
|
|
271
|
+
};
|
|
272
|
+
for (const s of collectMetaSessions(cwd, home))
|
|
273
|
+
pushSession(s);
|
|
274
|
+
for (const s of collectNativeTranscripts(home))
|
|
275
|
+
pushSession(s);
|
|
276
|
+
sessions.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0));
|
|
277
|
+
return { sessions, memories: collectRulesAndMemory(cwd, home) };
|
|
278
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AdapterContext } from "@agentoryhq/runtime-contract";
|
|
2
|
+
import type { DiscoveredAgent } from "@agentoryhq/shared";
|
|
3
|
+
export declare function detectCursor(ctx: AdapterContext): boolean;
|
|
4
|
+
export declare function discoverCursorConfig(ctx: AdapterContext): DiscoveredAgent | null;
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function readJsonFile(file) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function mcpFromCursorConfig(raw) {
|
|
12
|
+
if (typeof raw !== "object" || raw === null)
|
|
13
|
+
return [];
|
|
14
|
+
const servers = raw.mcpServers;
|
|
15
|
+
if (typeof servers !== "object" || servers === null)
|
|
16
|
+
return [];
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
19
|
+
if (!name)
|
|
20
|
+
continue;
|
|
21
|
+
let command;
|
|
22
|
+
if (typeof cfg === "object" && cfg !== null) {
|
|
23
|
+
const c = cfg;
|
|
24
|
+
if (typeof c.command === "string") {
|
|
25
|
+
const args = Array.isArray(c.args) ? c.args.map(String).join(" ") : "";
|
|
26
|
+
command = args ? `${c.command} ${args}` : c.command;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
out.push({ name, command, status: "unknown" });
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function skillsFromDir(dir) {
|
|
34
|
+
try {
|
|
35
|
+
return fs
|
|
36
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
37
|
+
.filter((d) => d.isDirectory() || (d.isFile() && d.name.endsWith(".md")))
|
|
38
|
+
.map((d) => d.name.replace(/\.md$/, ""));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function detectCursor(ctx) {
|
|
45
|
+
const cursorMcpCandidates = [
|
|
46
|
+
path.join(ctx.cwd, ".cursor", "mcp.json"),
|
|
47
|
+
path.join(ctx.homeDir, ".cursor", "mcp.json"),
|
|
48
|
+
];
|
|
49
|
+
for (const file of cursorMcpCandidates) {
|
|
50
|
+
if (fs.existsSync(file))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return fs.existsSync(path.join(ctx.cwd, ".cursor"));
|
|
54
|
+
}
|
|
55
|
+
export function discoverCursorConfig(ctx) {
|
|
56
|
+
if (!detectCursor(ctx))
|
|
57
|
+
return null;
|
|
58
|
+
const cursorMcpCandidates = [
|
|
59
|
+
path.join(ctx.cwd, ".cursor", "mcp.json"),
|
|
60
|
+
path.join(ctx.homeDir, ".cursor", "mcp.json"),
|
|
61
|
+
];
|
|
62
|
+
let cursorMcp = [];
|
|
63
|
+
for (const file of cursorMcpCandidates) {
|
|
64
|
+
if (!fs.existsSync(file))
|
|
65
|
+
continue;
|
|
66
|
+
cursorMcp = mcpFromCursorConfig(readJsonFile(file));
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const skillDirs = [
|
|
70
|
+
path.join(ctx.cwd, ".cursor", "skills"),
|
|
71
|
+
path.join(ctx.homeDir, ".cursor", "skills"),
|
|
72
|
+
path.join(ctx.cwd, ".cursor", "skills-cursor"),
|
|
73
|
+
path.join(ctx.homeDir, ".cursor", "skills-cursor"),
|
|
74
|
+
];
|
|
75
|
+
const skillNames = new Set();
|
|
76
|
+
for (const d of skillDirs) {
|
|
77
|
+
for (const name of skillsFromDir(d))
|
|
78
|
+
skillNames.add(name);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
runtime: "cursor",
|
|
82
|
+
supported: true,
|
|
83
|
+
displayName: `Cursor · ${path.basename(ctx.cwd) || "workspace"}`,
|
|
84
|
+
mcp: cursorMcp,
|
|
85
|
+
skills: [...skillNames],
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RuntimeAdapter } from "@agentoryhq/runtime-contract";
|
|
2
|
+
export { applyAlignPatch } from "./align.js";
|
|
3
|
+
export { buildCursorProjectPathMap, indexCursorSessions, titleFromTranscriptJsonl, } from "./asset-index.js";
|
|
4
|
+
export { detectCursor, discoverCursorConfig } from "./discover.js";
|
|
5
|
+
export { TRANSCRIPT_MESSAGE_MAX_CHARS, TRANSCRIPT_WINDOW_MESSAGES, captureTranscriptSnapshot, maskSecrets, transcriptFileForSourceKey, } from "./transcript.js";
|
|
6
|
+
export declare const cursorAdapter: RuntimeAdapter;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ADAPTER_API_VERSION } from "@agentoryhq/runtime-contract";
|
|
2
|
+
import { applyAlignPatch } from "./align.js";
|
|
3
|
+
import { indexCursorSessions } from "./asset-index.js";
|
|
4
|
+
import { detectCursor, discoverCursorConfig } from "./discover.js";
|
|
5
|
+
import { captureTranscriptSnapshot, transcriptFileForSourceKey } from "./transcript.js";
|
|
6
|
+
export { applyAlignPatch } from "./align.js";
|
|
7
|
+
export { buildCursorProjectPathMap, indexCursorSessions, titleFromTranscriptJsonl, } from "./asset-index.js";
|
|
8
|
+
export { detectCursor, discoverCursorConfig } from "./discover.js";
|
|
9
|
+
export { TRANSCRIPT_MESSAGE_MAX_CHARS, TRANSCRIPT_WINDOW_MESSAGES, captureTranscriptSnapshot, maskSecrets, transcriptFileForSourceKey, } from "./transcript.js";
|
|
10
|
+
export const cursorAdapter = {
|
|
11
|
+
manifest: {
|
|
12
|
+
runtime: "cursor",
|
|
13
|
+
adapterApi: ADAPTER_API_VERSION,
|
|
14
|
+
displayName: "Cursor",
|
|
15
|
+
capabilities: [
|
|
16
|
+
"detect",
|
|
17
|
+
"discoverConfig",
|
|
18
|
+
"applyAlign",
|
|
19
|
+
"indexSessions",
|
|
20
|
+
"readTranscript",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
detect: detectCursor,
|
|
24
|
+
discoverConfig: discoverCursorConfig,
|
|
25
|
+
applyAlign: applyAlignPatch,
|
|
26
|
+
indexSessions: indexCursorSessions,
|
|
27
|
+
transcriptFileForSourceKey,
|
|
28
|
+
captureTranscriptSnapshot,
|
|
29
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TranscriptSnapshot } from "@agentoryhq/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Node-side transcript capture for on-demand review (model B · S-tier).
|
|
4
|
+
* Whole pipeline is local: read JSONL → extract messages → mask secret-shaped
|
|
5
|
+
* strings → keep the near-end window. Only the masked, windowed snapshot ever
|
|
6
|
+
* leaves the machine; raw body is never logged (brief §5 AC-3/AC-9).
|
|
7
|
+
*/
|
|
8
|
+
/** 近端窗口上限(brief AC-4:具体值在此钉死) */
|
|
9
|
+
export declare const TRANSCRIPT_WINDOW_MESSAGES = 40;
|
|
10
|
+
/** 单条消息字符上限(同上) */
|
|
11
|
+
export declare const TRANSCRIPT_MESSAGE_MAX_CHARS = 2000;
|
|
12
|
+
/** Mask secret-shaped substrings BEFORE anything leaves the node (AC-3). */
|
|
13
|
+
export declare function maskSecrets(text: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Read the transcript at `file` and build a masked, windowed snapshot.
|
|
16
|
+
* Returns undefined when the file is missing/unreadable; an empty snapshot
|
|
17
|
+
* for a session with no extractable messages (brief AC-1 empty state).
|
|
18
|
+
*/
|
|
19
|
+
export declare function captureTranscriptSnapshot(file: string, now?: Date): TranscriptSnapshot | undefined;
|
|
20
|
+
export type TranscriptFetchOpts = {
|
|
21
|
+
homeDir?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a transcript file path for a session sourceKey
|
|
25
|
+
* (`cursor-transcript:<abs path>` as emitted by asset-index.ts).
|
|
26
|
+
* Only files under `~/.cursor/projects/` (or AGENTORY_CURSOR_ROOT override)
|
|
27
|
+
* qualify — no arbitrary path scanning (invariants §4).
|
|
28
|
+
*/
|
|
29
|
+
export declare function transcriptFileForSourceKey(sourceKey: string, homeDir?: string): string | undefined;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Node-side transcript capture for on-demand review (model B · S-tier).
|
|
6
|
+
* Whole pipeline is local: read JSONL → extract messages → mask secret-shaped
|
|
7
|
+
* strings → keep the near-end window. Only the masked, windowed snapshot ever
|
|
8
|
+
* leaves the machine; raw body is never logged (brief §5 AC-3/AC-9).
|
|
9
|
+
*/
|
|
10
|
+
/** 近端窗口上限(brief AC-4:具体值在此钉死) */
|
|
11
|
+
export const TRANSCRIPT_WINDOW_MESSAGES = 40;
|
|
12
|
+
/** 单条消息字符上限(同上) */
|
|
13
|
+
export const TRANSCRIPT_MESSAGE_MAX_CHARS = 2000;
|
|
14
|
+
const SECRET_PATTERNS = [
|
|
15
|
+
{ re: /\bsk-[A-Za-z0-9_\-]{16,}/g, mask: "sk-••••••••" },
|
|
16
|
+
{ re: /\bghp_[A-Za-z0-9]{20,}/g, mask: "ghp_••••••••" },
|
|
17
|
+
{ re: /\bgho_[A-Za-z0-9]{20,}/g, mask: "gho_••••••••" },
|
|
18
|
+
{ re: /\bgithub_pat_[A-Za-z0-9_]{20,}/g, mask: "github_pat_••••••••" },
|
|
19
|
+
{ re: /\bxox[baprs]-[A-Za-z0-9\-]{16,}/g, mask: "xox•-••••••••" },
|
|
20
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/g, mask: "AKIA••••••••" },
|
|
21
|
+
{ re: /(?:password|passwd|pwd|secret|token|api[_-]?key)\s*[:=]\s*\S+/gi, mask: "password=••••••••" },
|
|
22
|
+
// 长随机密钥形态(hex/base64 ≥ 32 字符,独立成词)
|
|
23
|
+
{ re: /\b[A-Za-z0-9+/]{40,}={0,2}\b/g, mask: "••••••••" },
|
|
24
|
+
{ re: /\b[0-9a-f]{32,}\b/gi, mask: "••••••••" },
|
|
25
|
+
];
|
|
26
|
+
/** Mask secret-shaped substrings BEFORE anything leaves the node (AC-3). */
|
|
27
|
+
export function maskSecrets(text) {
|
|
28
|
+
let out = text;
|
|
29
|
+
for (const { re, mask } of SECRET_PATTERNS) {
|
|
30
|
+
out = out.replace(re, mask);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function extractTextBlob(content) {
|
|
35
|
+
if (typeof content === "string")
|
|
36
|
+
return content;
|
|
37
|
+
if (Array.isArray(content)) {
|
|
38
|
+
const parts = [];
|
|
39
|
+
for (const c of content) {
|
|
40
|
+
if (typeof c === "string")
|
|
41
|
+
parts.push(c);
|
|
42
|
+
else if (c && typeof c === "object" && typeof c.text === "string") {
|
|
43
|
+
parts.push(c.text);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return parts.join(" ");
|
|
47
|
+
}
|
|
48
|
+
if (content && typeof content === "object" && typeof content.text === "string") {
|
|
49
|
+
return content.text;
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
function cleanTimestampTag(text) {
|
|
54
|
+
return text
|
|
55
|
+
.replace(/<timestamp>[\s\S]*?<\/timestamp>/gi, " ")
|
|
56
|
+
.replace(/<\/?user_query>/gi, " ")
|
|
57
|
+
.replace(/\s+/g, " ")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the transcript at `file` and build a masked, windowed snapshot.
|
|
62
|
+
* Returns undefined when the file is missing/unreadable; an empty snapshot
|
|
63
|
+
* for a session with no extractable messages (brief AC-1 empty state).
|
|
64
|
+
*/
|
|
65
|
+
export function captureTranscriptSnapshot(file, now = new Date()) {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(file, "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const sourceKey = `cursor-transcript:${file}`;
|
|
74
|
+
const all = [];
|
|
75
|
+
for (const line of raw.split("\n")) {
|
|
76
|
+
if (!line.trim())
|
|
77
|
+
continue;
|
|
78
|
+
let o;
|
|
79
|
+
try {
|
|
80
|
+
o = JSON.parse(line);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!o || typeof o !== "object")
|
|
86
|
+
continue;
|
|
87
|
+
const row = o;
|
|
88
|
+
if (row.role !== "user" && row.role !== "assistant")
|
|
89
|
+
continue;
|
|
90
|
+
let text = "";
|
|
91
|
+
if (typeof row.message === "string")
|
|
92
|
+
text = row.message;
|
|
93
|
+
else if (row.message && typeof row.message === "object") {
|
|
94
|
+
text = extractTextBlob(row.message.content);
|
|
95
|
+
}
|
|
96
|
+
text = cleanTimestampTag(text);
|
|
97
|
+
if (!text)
|
|
98
|
+
continue;
|
|
99
|
+
if (text.length > TRANSCRIPT_MESSAGE_MAX_CHARS) {
|
|
100
|
+
text = `${text.slice(0, TRANSCRIPT_MESSAGE_MAX_CHARS)}…`;
|
|
101
|
+
}
|
|
102
|
+
const ts = typeof row.ts === "string" ? row.ts : "";
|
|
103
|
+
all.push({ role: row.role, ts, text });
|
|
104
|
+
}
|
|
105
|
+
const windowMessages = all.length > TRANSCRIPT_WINDOW_MESSAGES
|
|
106
|
+
? all.slice(-TRANSCRIPT_WINDOW_MESSAGES)
|
|
107
|
+
: all;
|
|
108
|
+
const truncated = all.length > windowMessages.length;
|
|
109
|
+
return {
|
|
110
|
+
sourceKey,
|
|
111
|
+
messages: windowMessages.map((m) => ({ ...m, text: maskSecrets(m.text) })),
|
|
112
|
+
truncated,
|
|
113
|
+
windowTotal: truncated ? `${windowMessages.length} / ${all.length} 条` : undefined,
|
|
114
|
+
capturedAt: now.toISOString(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a transcript file path for a session sourceKey
|
|
119
|
+
* (`cursor-transcript:<abs path>` as emitted by asset-index.ts).
|
|
120
|
+
* Only files under `~/.cursor/projects/` (or AGENTORY_CURSOR_ROOT override)
|
|
121
|
+
* qualify — no arbitrary path scanning (invariants §4).
|
|
122
|
+
*/
|
|
123
|
+
export function transcriptFileForSourceKey(sourceKey, homeDir) {
|
|
124
|
+
const prefix = "cursor-transcript:";
|
|
125
|
+
if (!sourceKey.startsWith(prefix))
|
|
126
|
+
return undefined;
|
|
127
|
+
const file = sourceKey.slice(prefix.length);
|
|
128
|
+
const home = homeDir ?? (process.env.AGENTORY_CURSOR_ROOT?.trim() || os.homedir());
|
|
129
|
+
const root = path.join(home, ".cursor", "projects");
|
|
130
|
+
try {
|
|
131
|
+
const rel = path.relative(root, file);
|
|
132
|
+
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return file;
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentoryhq/adapter-cursor",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Agentory RuntimeAdapter for Cursor",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@agentoryhq/runtime-contract": "*",
|
|
19
|
+
"@agentoryhq/shared": "*"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/tunsuy/agentory-node.git",
|
|
24
|
+
"directory": "adapters/cursor"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
}
|
|
32
|
+
}
|