@zlnap/astrolabe 0.17.0 → 0.17.2
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 +1 -1
- package/lib/setup/args.js +4 -2
- package/lib/setup/clients.js +53 -6
- package/lib/setup/cursor.js +112 -0
- package/lib/setup/run.js +10 -1
- package/lib/setup/vendor/astrolabe-dsh-guard/README.md +5 -3
- package/lib/setup/vendor/astrolabe-dsh-guard/package.json +1 -1
- package/lib/setup/zcode.js +54 -4
- package/lib/setup.test.js +278 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ MCP 客户端(推荐一键配置):
|
|
|
12
12
|
npx -y @zlnap/astrolabe setup
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Detects Claude Code, Cursor, Codex, Windsurf, Cline, Roo, Continue, OpenCode, DeepSeek Harness, Codebuddy, Grok Build, Kimi Code, and ZCode. Merges MCP config (OpenCode V1/V2/jsonc; Grok `[mcp_servers.*]` TOML; ZCode `mcp.servers`; DSH installs the native guard plugin + `cordis.patch.yml` merge). Optional `--with-hooks` installs OpenCode's guard plugin and hook recipes for Codebuddy / Grok / Kimi / ZCode (ZCode: no `SessionEnd`,
|
|
15
|
+
Detects Claude Code, Cursor, Codex, Windsurf, Cline, Roo, Continue, OpenCode, DeepSeek Harness, Codebuddy, Grok Build, Kimi Code, and ZCode. Merges MCP config (OpenCode V1/V2/jsonc; Grok `[mcp_servers.*]` TOML; ZCode `mcp.servers`; DSH installs the native guard plugin + `cordis.patch.yml` merge). Optional `--with-hooks` installs OpenCode's guard plugin and hook recipes for Cursor / Codebuddy / Grok / Kimi / ZCode (Cursor: `~/.cursor/hooks.json` with `--client=cursor`; ZCode: no `SessionEnd`, scrub cleanup off `Stop`). Flags: `--yes`, `--clients=…`, `--dry-run`, `--with-hooks`, `--list`.
|
|
16
16
|
|
|
17
17
|
探测本机 Claude Code / Cursor / Codex 等,交互勾选后合并写入 MCP 配置(`--yes`、`--dry-run`、`--list` 可用)。手动配置示例:
|
|
18
18
|
|
package/lib/setup/args.js
CHANGED
|
@@ -13,8 +13,10 @@ Options:
|
|
|
13
13
|
--native Use command "astrolabe" on PATH instead of npx
|
|
14
14
|
--command=<bin> Override MCP command (default: npx, or astrolabe with --native)
|
|
15
15
|
--with-hooks Also install drift-guard hooks/plugins where supported
|
|
16
|
-
(OpenCode guard plugin; Codebuddy/Grok/Kimi/ZCode hook
|
|
17
|
-
DSH always installs its native guard)
|
|
16
|
+
(OpenCode guard plugin; Cursor/Codebuddy/Grok/Kimi/ZCode hook
|
|
17
|
+
recipes; DSH always installs its native guard).
|
|
18
|
+
Cursor writes ~/.cursor/hooks.json (--client=cursor).
|
|
19
|
+
ZCode: PreToolUse only — never SessionEnd, never Stop cleanup
|
|
18
20
|
--no-cursor-permissions
|
|
19
21
|
Skip merging Cursor mcpAllowlist
|
|
20
22
|
--cursor-permissions Force Cursor allowlist merge when Cursor is selected (default on)
|
package/lib/setup/clients.js
CHANGED
|
@@ -9,6 +9,7 @@ const codebuddyMod = require('./codebuddy');
|
|
|
9
9
|
const grokMod = require('./grok');
|
|
10
10
|
const kimicodeMod = require('./kimicode');
|
|
11
11
|
const zcodeMod = require('./zcode');
|
|
12
|
+
const cursorMod = require('./cursor');
|
|
12
13
|
const PACKAGE_NAME = '@zlnap/astrolabe';
|
|
13
14
|
const SERVER_NAME = 'astrolabe';
|
|
14
15
|
|
|
@@ -360,17 +361,60 @@ const CLIENTS = [
|
|
|
360
361
|
}
|
|
361
362
|
return { detected: evidence.length > 0, evidence };
|
|
362
363
|
},
|
|
363
|
-
plan({ home, launch }) {
|
|
364
|
+
plan({ home, launch, withHooks = false }) {
|
|
364
365
|
const filePath = path.join(home, '.cursor', 'mcp.json');
|
|
366
|
+
const hooksPath = cursorMod.resolveCursorHooksPath(home);
|
|
365
367
|
const entry = jsonServerEntry(launch);
|
|
366
|
-
|
|
368
|
+
const steps = [];
|
|
369
|
+
steps.push({
|
|
367
370
|
kind: 'json-mcp',
|
|
368
371
|
filePath,
|
|
369
|
-
entry,
|
|
370
372
|
summary: `merge mcpServers.astrolabe into ${filePath}`,
|
|
371
373
|
apply() {
|
|
372
374
|
const result = mergeJsonMcpFile(filePath, entry, { create: true });
|
|
373
|
-
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
375
|
+
return { ...result, entry, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
if (withHooks) {
|
|
379
|
+
steps.push({
|
|
380
|
+
kind: 'cursor-hooks',
|
|
381
|
+
filePath: hooksPath,
|
|
382
|
+
summary: `merge preToolUse/sessionEnd into ${hooksPath} (--client=cursor)`,
|
|
383
|
+
apply() {
|
|
384
|
+
const create = exists(path.join(home, '.cursor')) || exists(hooksPath);
|
|
385
|
+
const result = cursorMod.mergeCursorHooksFile(hooksPath, {
|
|
386
|
+
create,
|
|
387
|
+
existsFn: exists,
|
|
388
|
+
});
|
|
389
|
+
if (result.skipped) return result;
|
|
390
|
+
return { ...result, write: () => writeJsonAtomic(hooksPath, result.doc) };
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
kind: 'cursor',
|
|
396
|
+
filePath,
|
|
397
|
+
entry,
|
|
398
|
+
summary:
|
|
399
|
+
steps[0].summary +
|
|
400
|
+
(withHooks ? `; hooks → ${hooksPath} (--client=cursor, matcher incl. mcp__astrolabe__.*)` : ''),
|
|
401
|
+
steps,
|
|
402
|
+
apply() {
|
|
403
|
+
const results = [];
|
|
404
|
+
for (const step of steps) {
|
|
405
|
+
results.push(step.apply());
|
|
406
|
+
}
|
|
407
|
+
const primary = results.find((r) => !r.skipped) || results[0];
|
|
408
|
+
return {
|
|
409
|
+
...primary,
|
|
410
|
+
entry,
|
|
411
|
+
results,
|
|
412
|
+
write: () => {
|
|
413
|
+
for (const r of results) {
|
|
414
|
+
if (!r.skipped && r.write) r.write();
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
};
|
|
374
418
|
},
|
|
375
419
|
};
|
|
376
420
|
},
|
|
@@ -839,7 +883,7 @@ const CLIENTS = [
|
|
|
839
883
|
filePath,
|
|
840
884
|
summary:
|
|
841
885
|
`merge mcp.servers.astrolabe into ${filePath}` +
|
|
842
|
-
(withHooks ? ' (+ hooks.events.PreToolUse
|
|
886
|
+
(withHooks ? ' (+ hooks.events.PreToolUse; no SessionEnd; scrub Stop cleanup)' : ''),
|
|
843
887
|
apply() {
|
|
844
888
|
const create =
|
|
845
889
|
exists(path.join(home, '.zcode')) ||
|
|
@@ -898,7 +942,7 @@ module.exports = {
|
|
|
898
942
|
resolveDshPluginInstallDirs: dshMod.resolveDshPluginInstallDirs,
|
|
899
943
|
OPENCODE_GUARD_TS: opencodeMod.OPENCODE_GUARD_TS,
|
|
900
944
|
parseJsonc: require('./jsonc').parseJsonc,
|
|
901
|
-
// Codebuddy / Grok / Kimi / ZCode helpers (tests)
|
|
945
|
+
// Codebuddy / Grok / Kimi / ZCode / Cursor helpers (tests)
|
|
902
946
|
resolveCodebuddyMcpPath: codebuddyMod.resolveCodebuddyMcpPath,
|
|
903
947
|
mergeCodebuddyHooksSettings: codebuddyMod.mergeCodebuddyHooksSettings,
|
|
904
948
|
resolveGrokConfigToml: grokMod.resolveGrokConfigToml,
|
|
@@ -909,4 +953,7 @@ module.exports = {
|
|
|
909
953
|
mergeKimiHooksToml: kimicodeMod.mergeKimiHooksToml,
|
|
910
954
|
resolveZcodeConfigPath: zcodeMod.resolveZcodeConfigPath,
|
|
911
955
|
mergeZcodeConfigFile: zcodeMod.mergeZcodeConfigFile,
|
|
956
|
+
resolveCursorHooksPath: cursorMod.resolveCursorHooksPath,
|
|
957
|
+
mergeCursorHooksFile: cursorMod.mergeCursorHooksFile,
|
|
958
|
+
CURSOR_REMIND_MATCHER: cursorMod.REMIND_MATCHER,
|
|
912
959
|
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const REMIND_CMD = 'astrolabe hooks remind --client=cursor';
|
|
7
|
+
const CLEANUP_CMD = 'astrolabe hooks cleanup';
|
|
8
|
+
/** Matcher must include mcp__astrolabe__.* so successful Astrolabe calls reset counters. */
|
|
9
|
+
const REMIND_MATCHER = 'Shell|Read|Grep|mcp__astrolabe__.*';
|
|
10
|
+
|
|
11
|
+
function resolveCursorHooksPath(home) {
|
|
12
|
+
return path.join(home, '.cursor', 'hooks.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function remindEntry() {
|
|
16
|
+
return {
|
|
17
|
+
command: REMIND_CMD,
|
|
18
|
+
matcher: REMIND_MATCHER,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanupEntry() {
|
|
23
|
+
return {
|
|
24
|
+
command: CLEANUP_CMD,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function entryCommand(entry) {
|
|
29
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
30
|
+
if (typeof entry.command === 'string') return entry.command;
|
|
31
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
32
|
+
for (const h of hooks) {
|
|
33
|
+
if (h && typeof h.command === 'string') return h.command;
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isAstrolabeRemind(entry) {
|
|
39
|
+
return entryCommand(entry).includes('astrolabe hooks remind');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isAstrolabeCleanup(entry) {
|
|
43
|
+
return entryCommand(entry).includes('astrolabe hooks cleanup');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Merge ~/.cursor/hooks.json (Cursor CLI native format: camelCase events,
|
|
48
|
+
* flat command entries). Enforces --client=cursor and the documented matcher
|
|
49
|
+
* that includes mcp__astrolabe__.* so counter resets work.
|
|
50
|
+
*/
|
|
51
|
+
function mergeCursorHooksFile(filePath, { create = true, existsFn = fs.existsSync } = {}) {
|
|
52
|
+
let created = false;
|
|
53
|
+
let doc = {};
|
|
54
|
+
if (existsFn(filePath)) {
|
|
55
|
+
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
|
56
|
+
doc = JSON.parse(raw);
|
|
57
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
58
|
+
throw new Error(`${filePath} root must be a JSON object`);
|
|
59
|
+
}
|
|
60
|
+
} else if (!create) {
|
|
61
|
+
return { skipped: true, reason: `missing ${filePath} (will not create)` };
|
|
62
|
+
} else {
|
|
63
|
+
created = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (doc.version == null) doc.version = 1;
|
|
67
|
+
if (!doc.hooks || typeof doc.hooks !== 'object' || Array.isArray(doc.hooks)) {
|
|
68
|
+
doc.hooks = {};
|
|
69
|
+
}
|
|
70
|
+
const hooks = doc.hooks;
|
|
71
|
+
let replaced = false;
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(hooks.preToolUse)) hooks.preToolUse = [];
|
|
74
|
+
const remindIdx = hooks.preToolUse.findIndex(isAstrolabeRemind);
|
|
75
|
+
const nextRemind = remindEntry();
|
|
76
|
+
if (remindIdx >= 0) {
|
|
77
|
+
hooks.preToolUse[remindIdx] = nextRemind;
|
|
78
|
+
replaced = true;
|
|
79
|
+
} else {
|
|
80
|
+
hooks.preToolUse.push(nextRemind);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!Array.isArray(hooks.sessionEnd)) hooks.sessionEnd = [];
|
|
84
|
+
const cleanupIdx = hooks.sessionEnd.findIndex(isAstrolabeCleanup);
|
|
85
|
+
const nextCleanup = cleanupEntry();
|
|
86
|
+
if (cleanupIdx >= 0) {
|
|
87
|
+
hooks.sessionEnd[cleanupIdx] = nextCleanup;
|
|
88
|
+
replaced = true;
|
|
89
|
+
} else {
|
|
90
|
+
hooks.sessionEnd.push(nextCleanup);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
filePath,
|
|
95
|
+
doc,
|
|
96
|
+
created,
|
|
97
|
+
replaced,
|
|
98
|
+
entryPreview: JSON.stringify(
|
|
99
|
+
{ version: doc.version, hooks: { preToolUse: [nextRemind], sessionEnd: [nextCleanup] } },
|
|
100
|
+
null,
|
|
101
|
+
2,
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
REMIND_CMD,
|
|
108
|
+
CLEANUP_CMD,
|
|
109
|
+
REMIND_MATCHER,
|
|
110
|
+
resolveCursorHooksPath,
|
|
111
|
+
mergeCursorHooksFile,
|
|
112
|
+
};
|
package/lib/setup/run.js
CHANGED
|
@@ -261,6 +261,15 @@ function run(argv = process.argv.slice(2)) {
|
|
|
261
261
|
process.stdout.write(
|
|
262
262
|
' 3. Cursor: open Settings → MCP and confirm astrolabe is listed; enable Run Mode for allowlist.\n',
|
|
263
263
|
);
|
|
264
|
+
if (opts.withHooks) {
|
|
265
|
+
process.stdout.write(
|
|
266
|
+
' Hooks: ~/.cursor/hooks.json uses --client=cursor (matcher includes mcp__astrolabe__.*).\n',
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
process.stdout.write(
|
|
270
|
+
' Tip: re-run with --with-hooks to write ~/.cursor/hooks.json (--client=cursor).\n',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
264
273
|
}
|
|
265
274
|
if (selectedIds.includes('opencode')) {
|
|
266
275
|
process.stdout.write(
|
|
@@ -294,7 +303,7 @@ function run(argv = process.argv.slice(2)) {
|
|
|
294
303
|
}
|
|
295
304
|
if (selectedIds.includes('zcode')) {
|
|
296
305
|
process.stdout.write(
|
|
297
|
-
' 3. ZCode: MCP under mcp.servers in ~/.zcode/cli/config.json; tip: --with-hooks adds hooks.events.PreToolUse (no SessionEnd).\n',
|
|
306
|
+
' 3. ZCode: MCP under mcp.servers in ~/.zcode/cli/config.json; tip: --with-hooks adds hooks.events.PreToolUse (--client=zcode; no SessionEnd; scrub Stop cleanup).\n',
|
|
298
307
|
);
|
|
299
308
|
}
|
|
300
309
|
return summary.some((row) => row.status === 'written') || summary.every((row) => row.status === 'skipped')
|
|
@@ -84,9 +84,11 @@ and reset when the OS clears the temp directory.
|
|
|
84
84
|
|
|
85
85
|
- Astrolabe identifies its own tools by substring, so DSH's per-repository server
|
|
86
86
|
names (`mcp__astrolabe-<repo>__find_symbol`) need no rewriting.
|
|
87
|
-
-
|
|
88
|
-
grep/read
|
|
89
|
-
|
|
87
|
+
- Any Astrolabe catalog tool (including `search_code`) resets the
|
|
88
|
+
grep/read/non_symbolic/slice counters on PreToolUse and clears the
|
|
89
|
+
post-deny silence window. Prefer `find_symbol` /
|
|
90
|
+
`resolve_context` / `find_references` / `goto_definition` for navigation;
|
|
91
|
+
`search_code` is fine for recovery after a deny.
|
|
90
92
|
|
|
91
93
|
## Licence
|
|
92
94
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astrolabe-dsh-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Astrolabe drift guard for DeepSeek Harness: deny repeated grep/read calls and steer the model to Astrolabe's MCP code-navigation tools.",
|
|
6
6
|
"type": "module",
|
package/lib/setup/zcode.js
CHANGED
|
@@ -47,20 +47,54 @@ function preToolUseEntry() {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function
|
|
50
|
+
function hookCommands(entry) {
|
|
51
|
+
if (!entry || typeof entry !== 'object') return [];
|
|
52
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
53
|
+
return hooks
|
|
54
|
+
.filter((h) => h && typeof h.command === 'string')
|
|
55
|
+
.map((h) => h.command);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function entryHasAstrolabeRemind(entry) {
|
|
59
|
+
return hookCommands(entry).some((cmd) => cmd.includes('astrolabe hooks remind'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Strip cleanup hooks from a Stop entry; drop the entry if nothing remains.
|
|
64
|
+
* Returns { entry | null, stripped }.
|
|
65
|
+
*/
|
|
66
|
+
function scrubStopEntry(entry) {
|
|
67
|
+
if (!entry || typeof entry !== 'object') return { entry: null, stripped: false };
|
|
68
|
+
if (!entryHasCleanup(entry)) return { entry, stripped: false };
|
|
69
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
70
|
+
const kept = hooks.filter(
|
|
71
|
+
(h) => !(h && typeof h.command === 'string' && h.command.includes('astrolabe hooks cleanup')),
|
|
72
|
+
);
|
|
73
|
+
if (!kept.length) return { entry: null, stripped: true };
|
|
74
|
+
return { entry: { ...entry, hooks: kept }, stripped: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* True when a Stop (or similar) event entry hangs `astrolabe hooks cleanup`.
|
|
79
|
+
* ZCode fires Stop at every turn end — cleanup there would wipe counters each turn.
|
|
80
|
+
*/
|
|
81
|
+
function entryHasCleanup(entry) {
|
|
51
82
|
if (!entry || typeof entry !== 'object') return false;
|
|
52
83
|
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
53
84
|
return hooks.some(
|
|
54
85
|
(h) =>
|
|
55
86
|
h &&
|
|
56
87
|
typeof h.command === 'string' &&
|
|
57
|
-
h.command.includes('astrolabe hooks
|
|
88
|
+
h.command.includes('astrolabe hooks cleanup'),
|
|
58
89
|
);
|
|
59
90
|
}
|
|
60
91
|
|
|
92
|
+
|
|
61
93
|
/**
|
|
62
94
|
* Merge mcp.servers.astrolabe and (optional) hooks.events.PreToolUse into
|
|
63
|
-
* ~/.zcode/cli/config.json. Never writes SessionEnd; never hangs cleanup on Stop
|
|
95
|
+
* ~/.zcode/cli/config.json. Never writes SessionEnd; never hangs cleanup on Stop
|
|
96
|
+
* (Stop is turn-end — use `astrolabe hooks gc`). Replaces any prior Astrolabe
|
|
97
|
+
* remind (including wrong --client=claude-code).
|
|
64
98
|
*/
|
|
65
99
|
function mergeZcodeConfigFile(
|
|
66
100
|
filePath,
|
|
@@ -102,11 +136,27 @@ function mergeZcodeConfigFile(
|
|
|
102
136
|
// Hard rule: never SessionEnd (ZCode rejects unknown events).
|
|
103
137
|
if (Object.prototype.hasOwnProperty.call(doc.hooks.events, 'SessionEnd')) {
|
|
104
138
|
delete doc.hooks.events.SessionEnd;
|
|
139
|
+
hooksReplaced = true;
|
|
140
|
+
}
|
|
141
|
+
// Hard rule: never hang cleanup on Stop (fires every turn; would zero counters).
|
|
142
|
+
if (Array.isArray(doc.hooks.events.Stop)) {
|
|
143
|
+
const scrubbed = [];
|
|
144
|
+
for (const entry of doc.hooks.events.Stop) {
|
|
145
|
+
const { entry: next, stripped } = scrubStopEntry(entry);
|
|
146
|
+
if (stripped) hooksReplaced = true;
|
|
147
|
+
if (next) scrubbed.push(next);
|
|
148
|
+
}
|
|
149
|
+
if (scrubbed.length) {
|
|
150
|
+
doc.hooks.events.Stop = scrubbed;
|
|
151
|
+
} else {
|
|
152
|
+
delete doc.hooks.events.Stop;
|
|
153
|
+
}
|
|
105
154
|
}
|
|
106
155
|
if (!Array.isArray(doc.hooks.events.PreToolUse)) {
|
|
107
156
|
doc.hooks.events.PreToolUse = [];
|
|
108
157
|
}
|
|
109
|
-
|
|
158
|
+
// Match any prior remind (wrong --client=claude-code included) and replace.
|
|
159
|
+
const idx = doc.hooks.events.PreToolUse.findIndex(entryHasAstrolabeRemind);
|
|
110
160
|
const next = preToolUseEntry();
|
|
111
161
|
if (idx >= 0) {
|
|
112
162
|
doc.hooks.events.PreToolUse[idx] = next;
|
package/lib/setup.test.js
CHANGED
|
@@ -453,6 +453,15 @@ test('ZCode mcp.servers + hooks.events without SessionEnd', () => {
|
|
|
453
453
|
enabled: false,
|
|
454
454
|
events: {
|
|
455
455
|
SessionEnd: [{ hooks: [{ type: 'command', command: 'bad' }] }],
|
|
456
|
+
// Misconfigured: cleanup on Stop would wipe counters every turn
|
|
457
|
+
Stop: [
|
|
458
|
+
{
|
|
459
|
+
hooks: [
|
|
460
|
+
{ type: 'command', command: 'astrolabe hooks cleanup' },
|
|
461
|
+
{ type: 'command', command: 'echo keep-stop' },
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
],
|
|
456
465
|
PreToolUse: [{ hooks: [{ type: 'command', command: 'echo keep' }] }],
|
|
457
466
|
},
|
|
458
467
|
},
|
|
@@ -476,6 +485,15 @@ test('ZCode mcp.servers + hooks.events without SessionEnd', () => {
|
|
|
476
485
|
!JSON.stringify(doc.hooks).includes('SessionEnd'),
|
|
477
486
|
'SessionEnd must not appear in ZCode hooks',
|
|
478
487
|
);
|
|
488
|
+
// Stop keeps non-cleanup hooks; cleanup command is stripped
|
|
489
|
+
assert.ok(Array.isArray(doc.hooks.events.Stop));
|
|
490
|
+
assert.equal(doc.hooks.events.Stop.length, 1);
|
|
491
|
+
assert.equal(doc.hooks.events.Stop[0].hooks.length, 1);
|
|
492
|
+
assert.equal(doc.hooks.events.Stop[0].hooks[0].command, 'echo keep-stop');
|
|
493
|
+
assert.ok(
|
|
494
|
+
!JSON.stringify(doc.hooks).includes('hooks cleanup'),
|
|
495
|
+
'Stop must not hang astrolabe hooks cleanup',
|
|
496
|
+
);
|
|
479
497
|
assert.ok(
|
|
480
498
|
doc.hooks.events.PreToolUse.some((e) =>
|
|
481
499
|
(e.hooks || []).some((h) => String(h.command).includes('--client=zcode')),
|
|
@@ -509,6 +527,53 @@ test('ZCode mcp.servers + hooks.events without SessionEnd', () => {
|
|
|
509
527
|
}
|
|
510
528
|
});
|
|
511
529
|
|
|
530
|
+
test('ZCode withHooks strips Stop cleanup but keeps non-cleanup Stop', () => {
|
|
531
|
+
const home = tmpHome();
|
|
532
|
+
try {
|
|
533
|
+
const cli = path.join(home, '.zcode', 'cli');
|
|
534
|
+
fs.mkdirSync(cli, { recursive: true });
|
|
535
|
+
fs.writeFileSync(
|
|
536
|
+
path.join(cli, 'config.json'),
|
|
537
|
+
JSON.stringify(
|
|
538
|
+
{
|
|
539
|
+
hooks: {
|
|
540
|
+
enabled: true,
|
|
541
|
+
events: {
|
|
542
|
+
Stop: [
|
|
543
|
+
{
|
|
544
|
+
matcher: 'cleanup-only',
|
|
545
|
+
hooks: [{ type: 'command', command: 'astrolabe hooks cleanup' }],
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
matcher: 'other',
|
|
549
|
+
hooks: [{ type: 'command', command: 'echo turn-end' }],
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
null,
|
|
556
|
+
2,
|
|
557
|
+
),
|
|
558
|
+
);
|
|
559
|
+
const launch = { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.'] };
|
|
560
|
+
clientsMod
|
|
561
|
+
.getClient('zcode')
|
|
562
|
+
.plan({ home, launch, withHooks: true })
|
|
563
|
+
.apply()
|
|
564
|
+
.write();
|
|
565
|
+
const doc = JSON.parse(fs.readFileSync(path.join(cli, 'config.json'), 'utf8'));
|
|
566
|
+
assert.equal(doc.hooks.events.SessionEnd, undefined);
|
|
567
|
+
assert.ok(Array.isArray(doc.hooks.events.Stop));
|
|
568
|
+
assert.equal(doc.hooks.events.Stop.length, 1);
|
|
569
|
+
assert.equal(doc.hooks.events.Stop[0].matcher, 'other');
|
|
570
|
+
assert.equal(doc.hooks.events.Stop[0].hooks[0].command, 'echo turn-end');
|
|
571
|
+
assert.ok(!JSON.stringify(doc.hooks).includes('hooks cleanup'));
|
|
572
|
+
} finally {
|
|
573
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
512
577
|
test('setup --clients aliases resolve for new clients', () => {
|
|
513
578
|
const home = tmpHome();
|
|
514
579
|
try {
|
|
@@ -537,3 +602,216 @@ test('setup --clients aliases resolve for new clients', () => {
|
|
|
537
602
|
fs.rmSync(home, { recursive: true, force: true });
|
|
538
603
|
}
|
|
539
604
|
});
|
|
605
|
+
|
|
606
|
+
test('Cursor client context is always cursor (never claude-code)', () => {
|
|
607
|
+
const client = clientsMod.getClient('cursor');
|
|
608
|
+
assert.equal(client.context, 'cursor');
|
|
609
|
+
const launch = clientsMod.resolveLaunch({ context: client.context });
|
|
610
|
+
assert.ok(launch.args.includes('--context=cursor'));
|
|
611
|
+
assert.ok(!launch.args.some((a) => String(a).includes('claude-code')));
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('Cursor --with-hooks writes hooks.json with --client=cursor and mcp matcher', () => {
|
|
615
|
+
const home = tmpHome();
|
|
616
|
+
try {
|
|
617
|
+
fs.mkdirSync(path.join(home, '.cursor'), { recursive: true });
|
|
618
|
+
// Bad prior config: wrong client + matcher missing mcp__astrolabe__.*
|
|
619
|
+
fs.writeFileSync(
|
|
620
|
+
path.join(home, '.cursor', 'hooks.json'),
|
|
621
|
+
JSON.stringify(
|
|
622
|
+
{
|
|
623
|
+
version: 1,
|
|
624
|
+
hooks: {
|
|
625
|
+
preToolUse: [
|
|
626
|
+
{
|
|
627
|
+
command: 'astrolabe hooks remind --client=claude-code',
|
|
628
|
+
matcher: 'Shell|Read|Grep',
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
null,
|
|
634
|
+
2,
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
fs.writeFileSync(
|
|
638
|
+
path.join(home, '.cursor', 'mcp.json'),
|
|
639
|
+
JSON.stringify(
|
|
640
|
+
{
|
|
641
|
+
mcpServers: {
|
|
642
|
+
astrolabe: {
|
|
643
|
+
command: 'npx',
|
|
644
|
+
args: ['-y', '@zlnap/astrolabe', '.', '--context=claude-code'],
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
null,
|
|
649
|
+
2,
|
|
650
|
+
),
|
|
651
|
+
);
|
|
652
|
+
const result = spawnSync(
|
|
653
|
+
process.execPath,
|
|
654
|
+
[
|
|
655
|
+
BIN,
|
|
656
|
+
'setup',
|
|
657
|
+
'--yes',
|
|
658
|
+
'--clients=cursor',
|
|
659
|
+
'--with-hooks',
|
|
660
|
+
'--home',
|
|
661
|
+
home,
|
|
662
|
+
'--no-cursor-permissions',
|
|
663
|
+
],
|
|
664
|
+
{ encoding: 'utf8' },
|
|
665
|
+
);
|
|
666
|
+
assert.equal(result.status, 0, result.stderr + result.stdout);
|
|
667
|
+
|
|
668
|
+
const mcp = JSON.parse(fs.readFileSync(path.join(home, '.cursor', 'mcp.json'), 'utf8'));
|
|
669
|
+
assert.deepEqual(mcp.mcpServers.astrolabe.args, [
|
|
670
|
+
'-y',
|
|
671
|
+
'@zlnap/astrolabe',
|
|
672
|
+
'.',
|
|
673
|
+
'--context=cursor',
|
|
674
|
+
]);
|
|
675
|
+
assert.ok(!JSON.stringify(mcp).includes('claude-code'));
|
|
676
|
+
|
|
677
|
+
const hooks = JSON.parse(fs.readFileSync(path.join(home, '.cursor', 'hooks.json'), 'utf8'));
|
|
678
|
+
assert.equal(hooks.version, 1);
|
|
679
|
+
const remind = hooks.hooks.preToolUse.find((e) =>
|
|
680
|
+
String(e.command || '').includes('astrolabe hooks remind'),
|
|
681
|
+
);
|
|
682
|
+
assert.ok(remind, 'preToolUse remind missing');
|
|
683
|
+
assert.equal(remind.command, 'astrolabe hooks remind --client=cursor');
|
|
684
|
+
assert.equal(remind.matcher, clientsMod.CURSOR_REMIND_MATCHER);
|
|
685
|
+
assert.match(remind.matcher, /mcp__astrolabe__\.\*/);
|
|
686
|
+
assert.ok(!remind.command.includes('claude-code'));
|
|
687
|
+
const cleanup = hooks.hooks.sessionEnd.find((e) =>
|
|
688
|
+
String(e.command || '').includes('hooks cleanup'),
|
|
689
|
+
);
|
|
690
|
+
assert.ok(cleanup, 'sessionEnd cleanup missing');
|
|
691
|
+
// Idempotent: no duplicate reminds
|
|
692
|
+
const remindCount = hooks.hooks.preToolUse.filter((e) =>
|
|
693
|
+
String(e.command || '').includes('astrolabe hooks remind'),
|
|
694
|
+
).length;
|
|
695
|
+
assert.equal(remindCount, 1);
|
|
696
|
+
} finally {
|
|
697
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test('ZCode --with-hooks replaces wrong remind client and scrubs Stop cleanup', () => {
|
|
702
|
+
const home = tmpHome();
|
|
703
|
+
try {
|
|
704
|
+
const cli = path.join(home, '.zcode', 'cli');
|
|
705
|
+
fs.mkdirSync(cli, { recursive: true });
|
|
706
|
+
fs.writeFileSync(
|
|
707
|
+
path.join(cli, 'config.json'),
|
|
708
|
+
JSON.stringify(
|
|
709
|
+
{
|
|
710
|
+
mcp: { servers: {} },
|
|
711
|
+
hooks: {
|
|
712
|
+
enabled: true,
|
|
713
|
+
events: {
|
|
714
|
+
PreToolUse: [
|
|
715
|
+
{
|
|
716
|
+
matcher: '*',
|
|
717
|
+
hooks: [
|
|
718
|
+
{
|
|
719
|
+
type: 'command',
|
|
720
|
+
command: 'astrolabe hooks remind --client=claude-code',
|
|
721
|
+
timeout: 10,
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
Stop: [
|
|
727
|
+
{
|
|
728
|
+
matcher: '*',
|
|
729
|
+
hooks: [
|
|
730
|
+
{
|
|
731
|
+
type: 'command',
|
|
732
|
+
command: 'astrolabe hooks cleanup',
|
|
733
|
+
timeout: 10,
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
type: 'command',
|
|
737
|
+
command: 'echo keep-stop',
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
SessionEnd: [
|
|
743
|
+
{
|
|
744
|
+
hooks: [{ type: 'command', command: 'astrolabe hooks cleanup' }],
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
null,
|
|
751
|
+
2,
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
const client = clientsMod.getClient('zcode');
|
|
755
|
+
assert.equal(client.context, null);
|
|
756
|
+
const launch = clientsMod.resolveLaunch({ context: client.context });
|
|
757
|
+
assert.ok(!launch.args.some((a) => String(a).includes('--context=')));
|
|
758
|
+
const plan = client.plan({ home, launch, withHooks: true });
|
|
759
|
+
plan.apply().write();
|
|
760
|
+
const doc = JSON.parse(fs.readFileSync(path.join(cli, 'config.json'), 'utf8'));
|
|
761
|
+
assert.equal(doc.hooks.events.SessionEnd, undefined);
|
|
762
|
+
assert.ok(!JSON.stringify(doc.hooks).includes('SessionEnd'));
|
|
763
|
+
// Stop cleanup removed; non-cleanup Stop hook preserved
|
|
764
|
+
assert.ok(doc.hooks.events.Stop, 'Stop should remain when non-cleanup hooks survive');
|
|
765
|
+
const stopCmds = (doc.hooks.events.Stop || []).flatMap((e) =>
|
|
766
|
+
(e.hooks || []).map((h) => h.command),
|
|
767
|
+
);
|
|
768
|
+
assert.ok(stopCmds.includes('echo keep-stop'));
|
|
769
|
+
assert.ok(!stopCmds.some((c) => String(c).includes('hooks cleanup')));
|
|
770
|
+
// Wrong client replaced
|
|
771
|
+
const remindCmds = (doc.hooks.events.PreToolUse || []).flatMap((e) =>
|
|
772
|
+
(e.hooks || []).map((h) => h.command),
|
|
773
|
+
);
|
|
774
|
+
assert.ok(remindCmds.includes('astrolabe hooks remind --client=zcode'));
|
|
775
|
+
assert.ok(!remindCmds.some((c) => String(c).includes('--client=claude-code')));
|
|
776
|
+
// Pure Stop-cleanup-only configs drop Stop entirely
|
|
777
|
+
const home2 = tmpHome();
|
|
778
|
+
try {
|
|
779
|
+
const cli2 = path.join(home2, '.zcode', 'cli');
|
|
780
|
+
fs.mkdirSync(cli2, { recursive: true });
|
|
781
|
+
fs.writeFileSync(
|
|
782
|
+
path.join(cli2, 'config.json'),
|
|
783
|
+
JSON.stringify(
|
|
784
|
+
{
|
|
785
|
+
hooks: {
|
|
786
|
+
events: {
|
|
787
|
+
Stop: [
|
|
788
|
+
{
|
|
789
|
+
hooks: [{ type: 'command', command: 'astrolabe hooks cleanup' }],
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
null,
|
|
796
|
+
2,
|
|
797
|
+
),
|
|
798
|
+
);
|
|
799
|
+
clientsMod
|
|
800
|
+
.getClient('zcode')
|
|
801
|
+
.plan({ home: home2, launch, withHooks: true })
|
|
802
|
+
.apply()
|
|
803
|
+
.write();
|
|
804
|
+
const d2 = JSON.parse(fs.readFileSync(path.join(cli2, 'config.json'), 'utf8'));
|
|
805
|
+
assert.equal(d2.hooks.events.Stop, undefined);
|
|
806
|
+
assert.ok(
|
|
807
|
+
d2.hooks.events.PreToolUse.some((e) =>
|
|
808
|
+
(e.hooks || []).some((h) => h.command === 'astrolabe hooks remind --client=zcode'),
|
|
809
|
+
),
|
|
810
|
+
);
|
|
811
|
+
} finally {
|
|
812
|
+
fs.rmSync(home2, { recursive: true, force: true });
|
|
813
|
+
}
|
|
814
|
+
} finally {
|
|
815
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
816
|
+
}
|
|
817
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zlnap/astrolabe",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"mcpName": "io.github.wojiushirencai/astrolabe",
|
|
5
5
|
"description": "Local MCP server for code search and navigation (search_code, find_symbol, find_references, goto_definition) for AI coding agents. Node shim for the native Astrolabe AI binary.",
|
|
6
6
|
"keywords": [
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"node": ">=18"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@zlnap/darwin-arm64": "0.17.
|
|
44
|
-
"@zlnap/darwin-x64": "0.17.
|
|
45
|
-
"@zlnap/linux-x64-gnu": "0.17.
|
|
46
|
-
"@zlnap/linux-x64-musl": "0.17.
|
|
47
|
-
"@zlnap/linux-arm64-gnu": "0.17.
|
|
48
|
-
"@zlnap/win32-x64-msvc": "0.17.
|
|
43
|
+
"@zlnap/darwin-arm64": "0.17.2",
|
|
44
|
+
"@zlnap/darwin-x64": "0.17.2",
|
|
45
|
+
"@zlnap/linux-x64-gnu": "0.17.2",
|
|
46
|
+
"@zlnap/linux-x64-musl": "0.17.2",
|
|
47
|
+
"@zlnap/linux-arm64-gnu": "0.17.2",
|
|
48
|
+
"@zlnap/win32-x64-msvc": "0.17.2"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|