claude-spotter 1.5.13 → 1.6.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/CHANGELOG.md +19 -0
- package/bin/spotter.mjs +6 -0
- package/docs/11_dashboard-operations.md +1 -1
- package/package.json +1 -1
- package/src/cli/cursor-hook-cmd.mjs +242 -0
- package/src/cli/db-cmd.mjs +3 -3
- package/src/cli/install.mjs +28 -1
- package/src/core/runtime-error-store.mjs +2 -0
- package/src/host/adapters.mjs +13 -4
- package/src/platform/paths.mjs +5 -0
- package/src/tool-db/investigate-agents.mjs +1 -1
- package/src/tool-db/investigate-cursor.mjs +123 -0
- package/src/tool-db/investigate-mcp.mjs +2 -1
- package/src/tool-db/investigate-skills.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
各節はそのversion公開時点の変更記録であり、後続versionにより置換された仕様を含む。
|
|
4
4
|
現行runtime契約は[`docs/00_overview.md`](docs/00_overview.md)から辿る。
|
|
5
5
|
|
|
6
|
+
## 1.6.0 — 2026-08-24
|
|
7
|
+
|
|
8
|
+
- **Cursor を tool-db host として足した。** `src/host/adapters.mjs` に
|
|
9
|
+
`hostAgent: 'cursor'` / `tool-db.cursor.json` を追加し、MCP は `~/.cursor/mcp.json` と
|
|
10
|
+
project `.cursor/mcp.json`、skills / agents は `~/.cursor` と `.cursor` だけを読む。
|
|
11
|
+
Cursor 製品同梱の `skills-cursor` はカタログに入れない。Claude / Codex の DB は触らない。
|
|
12
|
+
- **`spotter cursor-hook`** は `~/.cursor/hooks.json` の `sessionStart` へ flat
|
|
13
|
+
`{command, timeout: 5}` を冪等マージする。工場 hook は残し、Cursor envelope を Claude 形へ
|
|
14
|
+
変換しない。`spotter install` は `~/.cursor` があるときこの hook を配線し、
|
|
15
|
+
`tool-db.cursor.json` を seed する。
|
|
16
|
+
|
|
17
|
+
## 1.5.14 — 2026-08-24
|
|
18
|
+
|
|
19
|
+
- **挙動不変のOS層整理(harness用語統一campaignの分離規約)。** Windows絶対パス表記
|
|
20
|
+
(ドライブレター/UNC)の判定を`src/platform/paths.mjs`の`isWindowsAbsolutePath()`へ集約し、
|
|
21
|
+
tool-db側の独自regexを委譲へ置換。runtime-error-storeの`killWorkerTree`には
|
|
22
|
+
`terminateProcessTree`と意図的に別物である理由(絶対にrejectしないbest-effort掃除)を明記した。
|
|
23
|
+
公開面・挙動は不変。
|
|
24
|
+
|
|
6
25
|
## 1.5.13 — 2026-08-23
|
|
7
26
|
|
|
8
27
|
- **OS依存を`src/platform/`へ集約。** 6ファイルへ重複していたWindowsのcmd.exe /c wrap・
|
package/bin/spotter.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { runStatus } from '../src/cli/status.mjs';
|
|
|
9
9
|
import { runDbList, runDbRefresh, runDbRebuild } from '../src/cli/db-cmd.mjs';
|
|
10
10
|
import { runCodexCommand } from '../src/cli/codex-cmd.mjs';
|
|
11
11
|
import { runCodexHookCommand } from '../src/cli/codex-hook-cmd.mjs';
|
|
12
|
+
import { runCursorHookCommand } from '../src/cli/cursor-hook-cmd.mjs';
|
|
12
13
|
import { runAuditorCommand } from '../src/cli/auditor-cmd.mjs';
|
|
13
14
|
import { runDiagnosticsCommand } from '../src/cli/diagnostics-cmd.mjs';
|
|
14
15
|
import { runEvaluationCommand } from '../src/cli/evaluation-cmd.mjs';
|
|
@@ -66,6 +67,8 @@ Usage:
|
|
|
66
67
|
run approved codex-sidecar worktree workflow
|
|
67
68
|
spotter codex-hook install|uninstall|diagnostics
|
|
68
69
|
(experimental) manage Codex native hooks
|
|
70
|
+
spotter cursor-hook install|uninstall|diagnostics
|
|
71
|
+
manage Cursor native catalog-refresh hooks
|
|
69
72
|
spotter auditor judge --stage STAGE --input FILE
|
|
70
73
|
(experimental) run primary auditor backend once
|
|
71
74
|
spotter auditor matrix --stage STAGE --input FILE
|
|
@@ -128,6 +131,9 @@ async function main() {
|
|
|
128
131
|
case 'codex-hook':
|
|
129
132
|
await runCodexHookCommand({ argv: rest });
|
|
130
133
|
return;
|
|
134
|
+
case 'cursor-hook':
|
|
135
|
+
await runCursorHookCommand({ argv: rest });
|
|
136
|
+
return;
|
|
131
137
|
case 'auditor':
|
|
132
138
|
await runAuditorCommand({ argv: rest });
|
|
133
139
|
return;
|
package/package.json
CHANGED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnRefreshDetached } from '../hooks/spawn-daemon.mjs';
|
|
7
|
+
import {
|
|
8
|
+
die,
|
|
9
|
+
findSpotterMarker,
|
|
10
|
+
isChildCall,
|
|
11
|
+
readStdinJson,
|
|
12
|
+
} from '../hooks/lib.mjs';
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const PACKAGE_ROOT = resolve(HERE, '..', '..');
|
|
16
|
+
const SPOTTER_BIN = join(PACKAGE_ROOT, 'bin', 'spotter.mjs');
|
|
17
|
+
const CURSOR_SESSION_START_TIMEOUT_SEC = 5;
|
|
18
|
+
const CURSOR_HOOK_FRAGMENT = 'cursor-hook session-start';
|
|
19
|
+
|
|
20
|
+
const CURSOR_HOOK_USAGE = `spotter cursor-hook — Cursor native hook adapter
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
spotter cursor-hook install [--cursor-home DIR]
|
|
24
|
+
spotter cursor-hook uninstall [--cursor-home DIR]
|
|
25
|
+
spotter cursor-hook diagnostics [--cursor-home DIR]
|
|
26
|
+
spotter cursor-hook session-start
|
|
27
|
+
|
|
28
|
+
Installs a Cursor sessionStart hook that refreshes tool-db.cursor.json.
|
|
29
|
+
Does not convert the Cursor envelope to Claude shape. Factory hooks stay in place.
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
export async function runCursorHookCommand({ argv = process.argv.slice(2) } = {}) {
|
|
33
|
+
const sub = argv[0];
|
|
34
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
35
|
+
process.stdout.write(CURSOR_HOOK_USAGE);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (sub === 'install') {
|
|
39
|
+
const opts = parseCursorHomeArgs(argv.slice(1));
|
|
40
|
+
const result = await installCursorHooks({ cursorHome: opts.cursorHome });
|
|
41
|
+
process.stdout.write(`${JSON.stringify({
|
|
42
|
+
installation: result.hooks.sessionStart === 'unchanged' ? 'already_wired' : 'wired',
|
|
43
|
+
hooksPath: result.hooksPath,
|
|
44
|
+
hooks: result.hooks,
|
|
45
|
+
}, null, 2)}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (sub === 'uninstall') {
|
|
49
|
+
const opts = parseCursorHomeArgs(argv.slice(1));
|
|
50
|
+
const result = await uninstallCursorHooks({ cursorHome: opts.cursorHome });
|
|
51
|
+
process.stdout.write(`${JSON.stringify({
|
|
52
|
+
hooksPath: result.hooksPath,
|
|
53
|
+
hooks: result.hooks,
|
|
54
|
+
}, null, 2)}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (sub === 'diagnostics') {
|
|
58
|
+
const opts = parseCursorHomeArgs(argv.slice(1));
|
|
59
|
+
const result = cursorHookDiagnostics({ cursorHome: opts.cursorHome });
|
|
60
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (sub === 'session-start') {
|
|
64
|
+
await runCursorSessionStartHook();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
process.stderr.write(`unknown cursor-hook subcommand: ${sub}\n${CURSOR_HOOK_USAGE}`);
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function runCursorSessionStartHook({
|
|
72
|
+
readInput = readStdinJson,
|
|
73
|
+
spawnRefreshDetachedFn = spawnRefreshDetached,
|
|
74
|
+
} = {}) {
|
|
75
|
+
if (isChildCall()) return;
|
|
76
|
+
const input = await readInput();
|
|
77
|
+
const cwd = cursorCwd(input);
|
|
78
|
+
if (!cwd) return;
|
|
79
|
+
const projectRoot = findSpotterMarker(cwd);
|
|
80
|
+
if (!projectRoot) return;
|
|
81
|
+
spawnRefreshDetachedFn({ projectRoot, hostAgent: 'cursor' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function cursorCwd(input) {
|
|
85
|
+
if (typeof input?.cwd === 'string' && input.cwd.length > 0) return input.cwd;
|
|
86
|
+
const root = input?.workspace_roots?.[0];
|
|
87
|
+
if (typeof root === 'string' && root.length > 0) return root;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function installCursorHooks({
|
|
92
|
+
cursorHome = defaultCursorHome(),
|
|
93
|
+
nodePath = process.execPath,
|
|
94
|
+
spotterBin = SPOTTER_BIN,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const hooksPath = join(cursorHome, 'hooks.json');
|
|
97
|
+
const file = await loadCursorHooks(hooksPath);
|
|
98
|
+
const command = hookCommand(nodePath, spotterBin);
|
|
99
|
+
const sessionStart = upsertSessionStart(file, command);
|
|
100
|
+
if (sessionStart === 'added') await persistCursorHooks(hooksPath, file);
|
|
101
|
+
return {
|
|
102
|
+
cursorHome,
|
|
103
|
+
hooksPath,
|
|
104
|
+
hooks: { sessionStart },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function uninstallCursorHooks({
|
|
109
|
+
cursorHome = defaultCursorHome(),
|
|
110
|
+
} = {}) {
|
|
111
|
+
const hooksPath = join(cursorHome, 'hooks.json');
|
|
112
|
+
const file = await loadCursorHooks(hooksPath);
|
|
113
|
+
const removed = removeSessionStart(file);
|
|
114
|
+
if (removed) await persistCursorHooks(hooksPath, file);
|
|
115
|
+
return {
|
|
116
|
+
cursorHome,
|
|
117
|
+
hooksPath,
|
|
118
|
+
hooks: { sessionStart: removed ? 'removed' : 'not present' },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function cursorHookDiagnostics({ cursorHome = defaultCursorHome() } = {}) {
|
|
123
|
+
const hooksPath = join(cursorHome, 'hooks.json');
|
|
124
|
+
const present = existsSync(hooksPath);
|
|
125
|
+
const file = present ? JSON.parse(readFileSync(hooksPath, 'utf8')) : { version: 1, hooks: {} };
|
|
126
|
+
if (file === null || typeof file !== 'object' || Array.isArray(file)) {
|
|
127
|
+
return {
|
|
128
|
+
installation: 'not-installed',
|
|
129
|
+
cursorHome,
|
|
130
|
+
hooksPath,
|
|
131
|
+
installedHooks: { sessionStart: false },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (file.hooks == null || typeof file.hooks !== 'object' || Array.isArray(file.hooks)) {
|
|
135
|
+
file.hooks = {};
|
|
136
|
+
}
|
|
137
|
+
const installed = listFor(file, 'sessionStart').some((entry) => isSpotterCursorHook(entry));
|
|
138
|
+
return {
|
|
139
|
+
installation: installed ? 'installed' : 'not-installed',
|
|
140
|
+
cursorHome,
|
|
141
|
+
hooksPath,
|
|
142
|
+
installedHooks: { sessionStart: installed },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isCursorHomePresent(cursorHome = defaultCursorHome()) {
|
|
147
|
+
return existsSync(cursorHome);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function defaultCursorHome() {
|
|
151
|
+
return join(homedir(), '.cursor');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseCursorHomeArgs(argv) {
|
|
155
|
+
let cursorHome = defaultCursorHome();
|
|
156
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
157
|
+
if (argv[index] === '--cursor-home') {
|
|
158
|
+
const value = argv[index + 1];
|
|
159
|
+
if (!value || value.startsWith('--')) {
|
|
160
|
+
throw Object.assign(new Error('--cursor-home requires a value'), { exitCode: 2 });
|
|
161
|
+
}
|
|
162
|
+
cursorHome = value;
|
|
163
|
+
index += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { cursorHome };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hookCommand(nodePath, spotterBin) {
|
|
170
|
+
return `${quoteArg(nodePath)} ${quoteArg(spotterBin)} cursor-hook session-start`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function quoteArg(value) {
|
|
174
|
+
const text = String(value);
|
|
175
|
+
if (!/[\s"']/.test(text)) return text;
|
|
176
|
+
return `"${text.replaceAll('"', '\\"')}"`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isSpotterCursorHook(entry) {
|
|
180
|
+
return typeof entry?.command === 'string' && entry.command.includes(CURSOR_HOOK_FRAGMENT);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function listFor(file, event) {
|
|
184
|
+
const current = file.hooks?.[event];
|
|
185
|
+
return Array.isArray(current) ? current : [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function upsertSessionStart(file, command) {
|
|
189
|
+
file.hooks ??= {};
|
|
190
|
+
const list = listFor(file, 'sessionStart');
|
|
191
|
+
for (const entry of list) {
|
|
192
|
+
if (typeof entry.command !== 'string') continue;
|
|
193
|
+
if (entry.command === command && entry.timeout === CURSOR_SESSION_START_TIMEOUT_SEC) {
|
|
194
|
+
return 'unchanged';
|
|
195
|
+
}
|
|
196
|
+
if (entry.command === command || isSpotterCursorHook(entry)) {
|
|
197
|
+
entry.command = command;
|
|
198
|
+
entry.timeout = CURSOR_SESSION_START_TIMEOUT_SEC;
|
|
199
|
+
file.hooks.sessionStart = list;
|
|
200
|
+
return 'added';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
list.push({ command, timeout: CURSOR_SESSION_START_TIMEOUT_SEC });
|
|
204
|
+
file.hooks.sessionStart = list;
|
|
205
|
+
return 'added';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function removeSessionStart(file) {
|
|
209
|
+
const list = listFor(file, 'sessionStart');
|
|
210
|
+
const kept = list.filter((entry) => !isSpotterCursorHook(entry));
|
|
211
|
+
if (kept.length === list.length) return false;
|
|
212
|
+
if (kept.length > 0) file.hooks.sessionStart = kept;
|
|
213
|
+
else delete file.hooks.sessionStart;
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function loadCursorHooks(path) {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
220
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
221
|
+
throw new Error(`${path} は object である必要があります`);
|
|
222
|
+
}
|
|
223
|
+
if (parsed.hooks == null) parsed.hooks = {};
|
|
224
|
+
if (typeof parsed.hooks !== 'object' || Array.isArray(parsed.hooks)) {
|
|
225
|
+
throw new Error(`${path} の hooks は object である必要があります`);
|
|
226
|
+
}
|
|
227
|
+
return parsed;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (err?.code === 'ENOENT') return { version: 1, hooks: {} };
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function persistCursorHooks(path, file) {
|
|
235
|
+
await mkdir(dirname(path), { recursive: true });
|
|
236
|
+
file.version = file.version ?? 1;
|
|
237
|
+
await writeFile(path, `${JSON.stringify(file, null, 2)}\n`, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`) {
|
|
241
|
+
runCursorHookCommand().catch((err) => die(err.message, err.exitCode ?? 2));
|
|
242
|
+
}
|
package/src/cli/db-cmd.mjs
CHANGED
|
@@ -14,9 +14,9 @@ import { writeFile } from 'node:fs/promises';
|
|
|
14
14
|
const DB_USAGE = `spotter db — manage the host-specific tool-db
|
|
15
15
|
|
|
16
16
|
Usage:
|
|
17
|
-
spotter db list [--host-agent claude|codex|automation]
|
|
18
|
-
spotter db refresh [--host-agent claude|codex|automation]
|
|
19
|
-
spotter db rebuild [--host-agent claude|codex|automation]
|
|
17
|
+
spotter db list [--host-agent claude|codex|automation|cursor]
|
|
18
|
+
spotter db refresh [--host-agent claude|codex|automation|cursor]
|
|
19
|
+
spotter db rebuild [--host-agent claude|codex|automation|cursor]
|
|
20
20
|
`;
|
|
21
21
|
|
|
22
22
|
function requireProjectRoot() {
|
package/src/cli/install.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { version as SPOTTER_VERSION } from '../version.mjs';
|
|
|
24
24
|
import { refresh } from '../tool-db/refresh.mjs';
|
|
25
25
|
import { localDbPath, globalDbPath } from '../tool-db/loader.mjs';
|
|
26
26
|
import { installCodexHooks } from './codex-hook-cmd.mjs';
|
|
27
|
+
import { installCursorHooks, isCursorHomePresent } from './cursor-hook-cmd.mjs';
|
|
27
28
|
import { prepareRuntimeErrorStoreDirectory } from '../core/runtime-error-store.mjs';
|
|
28
29
|
|
|
29
30
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -57,9 +58,12 @@ export async function runInstall({
|
|
|
57
58
|
cwd = process.cwd(),
|
|
58
59
|
skipRefresh = false,
|
|
59
60
|
skipCodexHooks = skipRefresh,
|
|
61
|
+
skipCursorHooks = skipRefresh,
|
|
60
62
|
refreshFn = refresh,
|
|
61
63
|
codexCliPresentFn = isCodexCliPresent,
|
|
62
64
|
installCodexHooksFn = installCodexHooks,
|
|
65
|
+
cursorHomePresentFn = isCursorHomePresent,
|
|
66
|
+
installCursorHooksFn = installCursorHooks,
|
|
63
67
|
prepareRuntimeErrorStoreDirectoryFn = prepareRuntimeErrorStoreDirectory,
|
|
64
68
|
auditorContext,
|
|
65
69
|
resolveDefaultAuditorContextFn = resolveDefaultAuditorContext,
|
|
@@ -150,6 +154,18 @@ export async function runInstall({
|
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
let cursorHooksRegistered = false;
|
|
158
|
+
if (target === 'project' && !skipCursorHooks) {
|
|
159
|
+
if (cursorHomePresentFn()) {
|
|
160
|
+
const result = await installCursorHooksFn();
|
|
161
|
+
cursorHooksRegistered = true;
|
|
162
|
+
console.log(' Cursor hooks registered');
|
|
163
|
+
console.log(` Cursor hooks: ${result.hooksPath}`);
|
|
164
|
+
} else {
|
|
165
|
+
console.log(' Cursor home not found — Cursor hooks not registered');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
// Seed the tool-db so the first session has something to audit against.
|
|
154
170
|
// Runs regardless of whether settings.json changed — re-running `spotter install`
|
|
155
171
|
// on an already-installed project is the canonical way to refresh tool-db drift
|
|
@@ -170,12 +186,18 @@ export async function runInstall({
|
|
|
170
186
|
console.log(` Codex local DB: ${localDbPath(cwd, 'codex')}`);
|
|
171
187
|
console.log(` Codex global DB: ${globalDbPath('codex')}`);
|
|
172
188
|
}
|
|
189
|
+
if (cursorHooksRegistered) {
|
|
190
|
+
const cursorResolved = await refreshFn({ projectRoot: cwd, hostAgent: 'cursor', logFn: log });
|
|
191
|
+
console.log(` ${cursorResolved.size} Cursor tool(s) resolved`);
|
|
192
|
+
console.log(` Cursor local DB: ${localDbPath(cwd, 'cursor')}`);
|
|
193
|
+
console.log(` Cursor global DB: ${globalDbPath('cursor')}`);
|
|
194
|
+
}
|
|
173
195
|
} catch (err) {
|
|
174
196
|
// §0: throw (fallback 禁止). But surface the recovery path so the user isn't
|
|
175
197
|
// left with "hooks registered, tool-db missing" and no clue what to run.
|
|
176
198
|
process.stderr.write(`\nspotter install: tool-db seeding failed.\n`);
|
|
177
199
|
process.stderr.write(` hooks are registered but tool-db is not ready.\n`);
|
|
178
|
-
process.stderr.write(` recover with: spotter db refresh and, for Codex, spotter db refresh --host-agent codex\n`);
|
|
200
|
+
process.stderr.write(` recover with: spotter db refresh and, for Codex, spotter db refresh --host-agent codex; for Cursor, spotter db refresh --host-agent cursor\n`);
|
|
179
201
|
throw err;
|
|
180
202
|
}
|
|
181
203
|
}
|
|
@@ -187,6 +209,11 @@ export async function runInstall({
|
|
|
187
209
|
} else if (target === 'project' && !skipCodexHooks) {
|
|
188
210
|
console.log(' Codex hooks are not active: rerun `spotter install` where `codex --version` succeeds');
|
|
189
211
|
}
|
|
212
|
+
if (cursorHooksRegistered) {
|
|
213
|
+
console.log(' Cursor catalog refresh is active: new Cursor Agent sessions refresh tool-db.cursor.json');
|
|
214
|
+
} else if (target === 'project' && !skipCursorHooks) {
|
|
215
|
+
console.log(' Cursor hooks are not active: rerun `spotter install` where ~/.cursor exists');
|
|
216
|
+
}
|
|
190
217
|
}
|
|
191
218
|
|
|
192
219
|
async function readExistingAuditorContext(markerPath, resolveDefaultAuditorContextFn) {
|
|
@@ -292,6 +292,8 @@ function runRuntimeWorker(workerPath, args, timeoutMs, signal) {
|
|
|
292
292
|
});
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
// platform/spawn.mjs の terminateProcessTree と似るが意図的に別物: こちらは受領記録の
|
|
296
|
+
// best-effort掃除で、絶対にrejectせず(SIGKILL直行・225ms fallback)呼び出し元を止めない。
|
|
295
297
|
function killWorkerTree(child) {
|
|
296
298
|
if (!Number.isSafeInteger(child?.pid) || child.pid <= 0) return Promise.resolve();
|
|
297
299
|
if (process.platform === 'win32') {
|
package/src/host/adapters.mjs
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// このtableだけが知る。呼び出し側(loader / refresh / CLI)は adapter を引いて
|
|
4
4
|
// 使うだけで、`if (hostAgent === 'codex')` 分岐を業務ロジックに書かない。
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// が持つ。片方のhostのrefreshがもう片方のDBをprune /
|
|
8
|
-
//
|
|
6
|
+
// ベンダー固有の実装本体は investigate-claude.mjs / investigate-codex.mjs /
|
|
7
|
+
// investigate-cursor.mjs が持つ。片方のhostのrefreshがもう片方のDBをprune /
|
|
8
|
+
// overwriteしない契約(AGENTS.md「ツールカタログはhost-local tool-db」)はこの分離が担保する。
|
|
9
9
|
|
|
10
10
|
import { buildInvestigationSnapshot } from '../tool-db/investigate-claude.mjs';
|
|
11
11
|
import { buildCodexInvestigationSnapshot } from '../tool-db/investigate-codex.mjs';
|
|
12
|
+
import { buildCursorInvestigationSnapshot } from '../tool-db/investigate-cursor.mjs';
|
|
12
13
|
|
|
13
14
|
const CLAUDE_ADAPTER = Object.freeze({
|
|
14
15
|
hostAgent: 'claude',
|
|
@@ -32,10 +33,18 @@ const AUTOMATION_ADAPTER = Object.freeze({
|
|
|
32
33
|
buildInvestigationSnapshot({ logFn, claudeBin, projectRoot }),
|
|
33
34
|
});
|
|
34
35
|
|
|
36
|
+
const CURSOR_ADAPTER = Object.freeze({
|
|
37
|
+
hostAgent: 'cursor',
|
|
38
|
+
toolDbFileName: 'tool-db.cursor.json',
|
|
39
|
+
buildSnapshot: ({ logFn, projectRoot }) =>
|
|
40
|
+
buildCursorInvestigationSnapshot({ logFn, projectRoot }),
|
|
41
|
+
});
|
|
42
|
+
|
|
35
43
|
const ADAPTERS = Object.freeze({
|
|
36
44
|
claude: CLAUDE_ADAPTER,
|
|
37
45
|
codex: CODEX_ADAPTER,
|
|
38
46
|
automation: AUTOMATION_ADAPTER,
|
|
47
|
+
cursor: CURSOR_ADAPTER,
|
|
39
48
|
});
|
|
40
49
|
|
|
41
50
|
export function normalizeToolDbHostAgent(hostAgent = 'claude') {
|
|
@@ -45,7 +54,7 @@ export function normalizeToolDbHostAgent(hostAgent = 'claude') {
|
|
|
45
54
|
if (Object.hasOwn(ADAPTERS, hostAgent)) {
|
|
46
55
|
return hostAgent;
|
|
47
56
|
}
|
|
48
|
-
throw new TypeError(`tool-db hostAgent must be claude, codex, or
|
|
57
|
+
throw new TypeError(`tool-db hostAgent must be claude, codex, automation, or cursor; got ${hostAgent}`);
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
export function getHostAdapter(hostAgent = 'claude') {
|
package/src/platform/paths.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export async function listAgentsAll({ logFn = () => {}, projectRoot } = {}) {
|
|
|
49
49
|
|
|
50
50
|
// Scan `<dir>/<name>.md` files. Returns Map<agent-name, description>. Missing directories
|
|
51
51
|
// or malformed agents are skipped silently.
|
|
52
|
-
async function scanAgentsDir(dir, logFn) {
|
|
52
|
+
export async function scanAgentsDir(dir, logFn) {
|
|
53
53
|
const out = new Map();
|
|
54
54
|
let entries;
|
|
55
55
|
try {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Cursor-native catalog investigation.
|
|
2
|
+
//
|
|
3
|
+
// Cursor の MCP / skills / agents は Claude / Codex の設定面とは別物なので、
|
|
4
|
+
// この snapshot は ~/.cursor と project の .cursor だけを読む。
|
|
5
|
+
// Claude の skills や Codex の mcp list を混ぜない。
|
|
6
|
+
// ~/.cursor/skills-cursor は Cursor 製品同梱であり、工場カタログに入れない。
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { scanAgentsDir } from './investigate-agents.mjs';
|
|
12
|
+
import { bellVisibleName, listMcpToolsOne } from './investigate-mcp.mjs';
|
|
13
|
+
import { scanSkillsDir } from './investigate-skills.mjs';
|
|
14
|
+
import { describeServer } from './mcp-config.mjs';
|
|
15
|
+
|
|
16
|
+
export async function buildCursorInvestigationSnapshot({
|
|
17
|
+
logFn = () => {},
|
|
18
|
+
projectRoot,
|
|
19
|
+
cursorHome = join(homedir(), '.cursor'),
|
|
20
|
+
} = {}) {
|
|
21
|
+
const snapshot = new Map();
|
|
22
|
+
|
|
23
|
+
const mcp = await listCursorMcpToolsAll({ logFn, projectRoot, cursorHome });
|
|
24
|
+
for (const [serverName, tools] of mcp.entries()) {
|
|
25
|
+
for (const tool of tools) {
|
|
26
|
+
if (!tool.description || tool.description.length === 0) continue;
|
|
27
|
+
snapshot.set(bellVisibleName(serverName, tool.name), tool.description);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const skills = await listCursorSkillsAll({ logFn, projectRoot, cursorHome });
|
|
32
|
+
for (const [name, description] of skills) {
|
|
33
|
+
snapshot.set(name, description);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const agents = await listCursorAgentsAll({ logFn, projectRoot, cursorHome });
|
|
37
|
+
for (const [name, description] of agents) {
|
|
38
|
+
snapshot.set(name, description);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return snapshot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function listCursorMcpToolsAll({
|
|
45
|
+
logFn = () => {},
|
|
46
|
+
projectRoot,
|
|
47
|
+
cursorHome = join(homedir(), '.cursor'),
|
|
48
|
+
} = {}) {
|
|
49
|
+
const servers = await listCursorMcpServers({ projectRoot, cursorHome });
|
|
50
|
+
const out = new Map();
|
|
51
|
+
for (const server of servers) {
|
|
52
|
+
try {
|
|
53
|
+
const tools = await listMcpToolsOne({ server, logFn, projectRoot });
|
|
54
|
+
out.set(server.name, tools);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logFn(`cursor mcp investigate failed for "${server.name}": ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function listCursorMcpServers({
|
|
63
|
+
projectRoot,
|
|
64
|
+
cursorHome = join(homedir(), '.cursor'),
|
|
65
|
+
} = {}) {
|
|
66
|
+
const user = await readMcpServersFile(join(cursorHome, 'mcp.json'));
|
|
67
|
+
const project = projectRoot ? await readMcpServersFile(join(projectRoot, '.cursor', 'mcp.json')) : {};
|
|
68
|
+
const merged = { ...user, ...project };
|
|
69
|
+
const servers = [];
|
|
70
|
+
for (const [name, entry] of Object.entries(merged)) {
|
|
71
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
72
|
+
const described = describeServer(name, entry);
|
|
73
|
+
if (described) servers.push(described);
|
|
74
|
+
}
|
|
75
|
+
return servers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function listCursorSkillsAll({
|
|
79
|
+
logFn = () => {},
|
|
80
|
+
projectRoot,
|
|
81
|
+
cursorHome = join(homedir(), '.cursor'),
|
|
82
|
+
} = {}) {
|
|
83
|
+
const out = new Map();
|
|
84
|
+
for (const [name, description] of await scanSkillsDir(join(cursorHome, 'skills'), logFn)) {
|
|
85
|
+
out.set(name, description);
|
|
86
|
+
}
|
|
87
|
+
if (projectRoot) {
|
|
88
|
+
for (const [name, description] of await scanSkillsDir(join(projectRoot, '.cursor', 'skills'), logFn)) {
|
|
89
|
+
out.set(name, description);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function listCursorAgentsAll({
|
|
96
|
+
logFn = () => {},
|
|
97
|
+
projectRoot,
|
|
98
|
+
cursorHome = join(homedir(), '.cursor'),
|
|
99
|
+
} = {}) {
|
|
100
|
+
const out = new Map();
|
|
101
|
+
for (const [name, description] of await scanAgentsDir(join(cursorHome, 'agents'), logFn)) {
|
|
102
|
+
out.set(name, description);
|
|
103
|
+
}
|
|
104
|
+
if (projectRoot) {
|
|
105
|
+
for (const [name, description] of await scanAgentsDir(join(projectRoot, '.cursor', 'agents'), logFn)) {
|
|
106
|
+
out.set(name, description);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function readMcpServersFile(path) {
|
|
113
|
+
let text;
|
|
114
|
+
try {
|
|
115
|
+
text = await readFile(path, 'utf8');
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err.code === 'ENOENT') return {};
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
const data = JSON.parse(text);
|
|
121
|
+
if (!data || typeof data !== 'object') return {};
|
|
122
|
+
return (data.mcpServers && typeof data.mcpServers === 'object') ? data.mcpServers : {};
|
|
123
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// 5. ← tools/list result (response with tools[] each having {name, description})
|
|
11
11
|
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
+
import { isWindowsAbsolutePath } from '../platform/paths.mjs';
|
|
13
14
|
import { execFileWindowsSafe, windowsCompatibleCommand } from '../platform/spawn.mjs';
|
|
14
15
|
import { listToolsHttp } from './investigate-mcp-http.mjs';
|
|
15
16
|
import { readMcpServers, describeServer } from './mcp-config.mjs';
|
|
@@ -186,7 +187,7 @@ export function splitCommandLine(s) {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
function extractUnquotedWindowsExecutable(s) {
|
|
189
|
-
if (
|
|
190
|
+
if (!isWindowsAbsolutePath(s)) return null;
|
|
190
191
|
const match = s.match(/^(.+?\.(?:exe|cmd|bat))(?:\s+|$)(.*)$/iu);
|
|
191
192
|
if (!match) return null;
|
|
192
193
|
return { command: match[1], rest: match[2].trim() };
|
|
@@ -55,7 +55,7 @@ export async function listSkillsAll({ logFn = () => {}, projectRoot } = {}) {
|
|
|
55
55
|
|
|
56
56
|
// Scan `<dir>/<name>/SKILL.md` files. Returns Map<skill-name, description>. Missing
|
|
57
57
|
// directories or malformed skills are skipped silently.
|
|
58
|
-
async function scanSkillsDir(dir, logFn) {
|
|
58
|
+
export async function scanSkillsDir(dir, logFn) {
|
|
59
59
|
const out = new Map();
|
|
60
60
|
let entries;
|
|
61
61
|
try {
|