@zlnap/astrolabe 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/bin/astrolabe.js +24 -10
- package/lib/setup/args.js +85 -0
- package/lib/setup/clients.js +912 -0
- package/lib/setup/codebuddy.js +93 -0
- package/lib/setup/dsh.js +137 -0
- package/lib/setup/grok.js +81 -0
- package/lib/setup/index.js +16 -0
- package/lib/setup/jsonc.js +334 -0
- package/lib/setup/kimicode.js +94 -0
- package/lib/setup/opencode.js +159 -0
- package/lib/setup/prompt.js +69 -0
- package/lib/setup/run.js +305 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/README.md +93 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/cordis.patch.yml +11 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/lib/index.js +145 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/package.json +38 -0
- package/lib/setup/zcode.js +135 -0
- package/lib/setup.test.js +539 -0
- package/package.json +7 -7
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# astrolabe-dsh-guard — Astrolabe drift guard for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
A **native DeepSeek Harness (DSH) hook plugin** that runs `astrolabe hooks remind`
|
|
4
|
+
on every relevant tool call and denies the call when Astrolabe says the agent has
|
|
5
|
+
drifted into repeated `grep`/`read` instead of using Astrolabe's MCP tools.
|
|
6
|
+
|
|
7
|
+
Unlike the Claude Code marketplace plugin in `../claude-plugin` (which only
|
|
8
|
+
registers the MCP server), this package is for **DSH hosts**, whose hook surface
|
|
9
|
+
is a Cordis plugin API rather than a `hooks.json` file.
|
|
10
|
+
|
|
11
|
+
## Why a native plugin
|
|
12
|
+
|
|
13
|
+
DSH's own design note is explicit:
|
|
14
|
+
|
|
15
|
+
> "native hooks" are not a package — a native hook is just an ordinary Cordis
|
|
16
|
+
> plugin subscribing to the canonical lifecycle events. … Anything a bridge can
|
|
17
|
+
> do, a plain plugin can do directly — more powerfully (no serialization
|
|
18
|
+
> boundary, full `ctx`, typed returns).
|
|
19
|
+
|
|
20
|
+
Running Astrolabe's hook through `@deepseek-ai/dsh-hooks-claude-code` also works,
|
|
21
|
+
but that path needs a third-party bridge package, a `hooks.json`, and a wrapper
|
|
22
|
+
script. This plugin needs none of them.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- `astrolabe` on `PATH` (see the repository README), or set `config.binary` to an
|
|
27
|
+
absolute path.
|
|
28
|
+
- A DSH profile where the plugin can be loaded (see *Install* below).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. put the package where the DSH profile resolves modules
|
|
34
|
+
cp -R dsh-plugin "$HOME/.dsh/profiles/desktop/node_modules/astrolabe-dsh-guard"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
# 2. ~/.dsh/cordis.patch.yml — insert the plugin
|
|
39
|
+
- insert:
|
|
40
|
+
- id: astrolabe-guard
|
|
41
|
+
name: 'astrolabe-dsh-guard'
|
|
42
|
+
config:
|
|
43
|
+
client: claude-code
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
DSH hot-reloads the patch file; no restart is required.
|
|
47
|
+
|
|
48
|
+
> The package declares `dsh.bundle`, so a DSH plugin manager that installs
|
|
49
|
+
> bundles can also take it (from a git URL or a local path) and manage it as a
|
|
50
|
+
> normal entry.
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
| Key | Default | Meaning |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `binary` | `astrolabe` | Executable to run; an absolute path is safest |
|
|
57
|
+
| `client` | `claude-code` | `--client` dialect passed to `astrolabe hooks remind` |
|
|
58
|
+
| `timeoutMs` | `20000` | Per-call timeout for the hook process |
|
|
59
|
+
| `stateDir` | `<tmpdir>/astrolabe-dsh-guard` | `HOME` for the hook process; Astrolabe writes its counter under `$HOME/.astrolabe` |
|
|
60
|
+
|
|
61
|
+
## The sandbox detail that matters
|
|
62
|
+
|
|
63
|
+
DSH runs hook commands inside its file sandbox. The user's home is readable but
|
|
64
|
+
**not writable** there, so Astrolabe's counter write to `~/.astrolabe/hook_data/`
|
|
65
|
+
fails and the hook silently fails open — the guard appears to be installed but
|
|
66
|
+
never denies anything.
|
|
67
|
+
|
|
68
|
+
The plugin therefore points the hook process at a writable `HOME`
|
|
69
|
+
(`stateDir`) instead of widening the sandbox. Counters live under
|
|
70
|
+
`<tmpdir>/astrolabe-dsh-guard/.astrolabe/hook_data/<session_id>/counter.json`
|
|
71
|
+
and reset when the OS clears the temp directory.
|
|
72
|
+
|
|
73
|
+
## Behaviour
|
|
74
|
+
|
|
75
|
+
- Tools observed: `grep`, `read`, `glob`, `bash`/`shell`, `rg`, plus every
|
|
76
|
+
Astrolabe MCP tool (so that using Astrolabe resets the counter).
|
|
77
|
+
- Default threshold: the 3rd consecutive `grep`/`read` without an intervening
|
|
78
|
+
symbolic Astrolabe call is denied, with Astrolabe's own reason text forwarded
|
|
79
|
+
to the model.
|
|
80
|
+
- Any failure (missing binary, timeout, unparsable output, sandbox fault) **fails
|
|
81
|
+
open**: the tool call proceeds.
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- Astrolabe identifies its own tools by substring, so DSH's per-repository server
|
|
86
|
+
names (`mcp__astrolabe-<repo>__find_symbol`) need no rewriting.
|
|
87
|
+
- `search_code` is a *non-symbolic* Astrolabe tool: it does not reset the
|
|
88
|
+
grep/read counter (by Astrolabe's own design). Use `find_symbol` /
|
|
89
|
+
`resolve_context` / `find_references` / `goto_definition` to reset it.
|
|
90
|
+
|
|
91
|
+
## Licence
|
|
92
|
+
|
|
93
|
+
MIT OR Apache-2.0, matching the rest of Astrolabe.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Bundle patch for DeepSeek Harness: inserts the Astrolabe drift guard plugin.
|
|
2
|
+
#
|
|
3
|
+
# Installed through the harness plugin manager (it declares `dsh.bundle`), or
|
|
4
|
+
# copied into a profile and referenced from the profile's own cordis.patch.yml.
|
|
5
|
+
# `astrolabe` must be on PATH for the plugin's `shell` calls; override the
|
|
6
|
+
# binary path with `config.binary` when it is not.
|
|
7
|
+
- insert:
|
|
8
|
+
- id: astrolabe-guard
|
|
9
|
+
name: 'astrolabe-dsh-guard'
|
|
10
|
+
config:
|
|
11
|
+
client: claude-code
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astrolabe drift guard for DeepSeek Harness — native hook plugin.
|
|
3
|
+
*
|
|
4
|
+
* DeepSeek Harness has no hook config file of its own: a "native hook" is an
|
|
5
|
+
* ordinary Cordis plugin subscribing to the canonical interception events, and
|
|
6
|
+
* the `dsh-hooks-claude-code` / `dsh-hooks-codex` packages are only translators
|
|
7
|
+
* that map an external shell-hook protocol onto that same API. This plugin skips
|
|
8
|
+
* the translation: it runs `astrolabe hooks remind` on `tools/pre-execute` and
|
|
9
|
+
* maps the returned decision onto the harness's typed `PreToolDecision`.
|
|
10
|
+
*
|
|
11
|
+
* Astrolabe recognises its own MCP tools by substring, so a per-repository
|
|
12
|
+
* server name such as `mcp__astrolabe-dict__find_symbol` already resets the
|
|
13
|
+
* consecutive grep/read counter; no tool-name rewriting is needed here.
|
|
14
|
+
*
|
|
15
|
+
* @module astrolabe-dsh-guard
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync } from 'node:fs'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
export const name = 'astrolabe-guard'
|
|
23
|
+
|
|
24
|
+
/** The command runner, the same seam the hook bridges use. */
|
|
25
|
+
export const inject = ['shell']
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tools whose calls change Astrolabe's drift counter.
|
|
29
|
+
*
|
|
30
|
+
* `grep`, `read`, `glob` and shell text search accumulate the counter; every
|
|
31
|
+
* Astrolabe tool call must also reach the hook so the counter resets — a denied
|
|
32
|
+
* grep otherwise stays denied until the idle timeout. Unrelated tools are
|
|
33
|
+
* skipped to avoid one process spawn per tool call.
|
|
34
|
+
*/
|
|
35
|
+
const RELEVANT = /^(grep|read|glob|bash|shell|rg)$/i
|
|
36
|
+
|
|
37
|
+
/** Astrolabe tool names as they appear after an MCP server prefix. */
|
|
38
|
+
const ASTROLABE_TOOL =
|
|
39
|
+
/(?:^|__)(search_code|find_symbol|find_references|goto_definition|resolve_context|get_diagnostics|get_languages)$/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether this call can change Astrolabe's drift counter.
|
|
43
|
+
* @param toolName - the harness tool name, e.g. `grep` or `mcp__astrolabe-dict__search_code`.
|
|
44
|
+
* @returns true when the hook must observe the call.
|
|
45
|
+
*/
|
|
46
|
+
function relevant(toolName) {
|
|
47
|
+
return RELEVANT.test(toolName) || (toolName.includes('astrolabe') && ASTROLABE_TOOL.test(toolName))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the `PreToolUse` payload `astrolabe hooks remind` parses.
|
|
52
|
+
* @param exec - the tool execution being gated.
|
|
53
|
+
* @returns the payload object.
|
|
54
|
+
*/
|
|
55
|
+
function payloadOf(exec) {
|
|
56
|
+
return {
|
|
57
|
+
session_id: exec.agent?.session?.header?.id ?? '',
|
|
58
|
+
transcript_path: '',
|
|
59
|
+
cwd: exec.agent?.session?.header?.cwd ?? process.cwd(),
|
|
60
|
+
hook_event_name: 'PreToolUse',
|
|
61
|
+
tool_name: exec.name,
|
|
62
|
+
tool_input: exec.arguments,
|
|
63
|
+
tool_use_id: exec.callId,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run `astrolabe hooks remind` and decode its decision.
|
|
69
|
+
* @param shell - the harness shell executor.
|
|
70
|
+
* @param spec - binary, client dialect, timeout, state home and the payload to pipe in.
|
|
71
|
+
* @returns the decoded hook output, or undefined when it printed nothing usable.
|
|
72
|
+
*/
|
|
73
|
+
async function remind(shell, spec) {
|
|
74
|
+
const request = {
|
|
75
|
+
command: `${spec.binary} hooks remind --client=${spec.client}`,
|
|
76
|
+
timeoutMs: spec.timeoutMs,
|
|
77
|
+
stdin: JSON.stringify(spec.payload),
|
|
78
|
+
// Astrolabe persists its consecutive-call counter under `$HOME/.astrolabe`.
|
|
79
|
+
// This command runs inside the harness file sandbox, where the user's home
|
|
80
|
+
// is readable but not writable, so the counter write would fail and the hook
|
|
81
|
+
// would silently fail open. A writable state home keeps the counter alive
|
|
82
|
+
// without widening the sandbox.
|
|
83
|
+
env: { HOME: spec.stateHome },
|
|
84
|
+
...(spec.signal === undefined ? {} : { signal: spec.signal }),
|
|
85
|
+
}
|
|
86
|
+
const result = await (await shell.execute(shell.resolve(request))).result()
|
|
87
|
+
const text = (result.stdout?.text ?? '').trim()
|
|
88
|
+
if (text === '') return undefined
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(text)
|
|
91
|
+
} catch {
|
|
92
|
+
// A malformed decision is not a blocking one: fail open.
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Install the guard.
|
|
99
|
+
* @param ctx - the plugin context, providing `shell` and the tool events.
|
|
100
|
+
* @param config - plugin configuration (all optional).
|
|
101
|
+
*/
|
|
102
|
+
export function apply(ctx, config = {}) {
|
|
103
|
+
const binary = typeof config.binary === 'string' && config.binary !== '' ? config.binary : 'astrolabe'
|
|
104
|
+
const client = typeof config.client === 'string' && config.client !== '' ? config.client : 'claude-code'
|
|
105
|
+
const timeoutMs = Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : 20000
|
|
106
|
+
const stateHome = typeof config.stateDir === 'string' && config.stateDir !== ''
|
|
107
|
+
? config.stateDir
|
|
108
|
+
: join(tmpdir(), 'astrolabe-dsh-guard')
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
mkdirSync(stateHome, { recursive: true })
|
|
112
|
+
} catch {
|
|
113
|
+
// A state home we cannot create leaves the hook fail-open, never blocking.
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
117
|
+
if (!relevant(exec.name)) return next()
|
|
118
|
+
|
|
119
|
+
let output
|
|
120
|
+
try {
|
|
121
|
+
output = await remind(ctx.shell, {
|
|
122
|
+
binary,
|
|
123
|
+
client,
|
|
124
|
+
timeoutMs,
|
|
125
|
+
stateHome,
|
|
126
|
+
payload: payloadOf(exec),
|
|
127
|
+
signal: exec.signal,
|
|
128
|
+
})
|
|
129
|
+
} catch {
|
|
130
|
+
// A hook that cannot run must never block the turn — same rule as the
|
|
131
|
+
// harness's own hook bridges.
|
|
132
|
+
return next()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const decision = output?.hookSpecificOutput
|
|
136
|
+
if (decision?.permissionDecision === 'deny') {
|
|
137
|
+
return {
|
|
138
|
+
kind: 'deny',
|
|
139
|
+
reason: decision.permissionDecisionReason
|
|
140
|
+
?? 'Astrolabe: use the MCP code-navigation tools instead of grep/read',
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return next()
|
|
144
|
+
})
|
|
145
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astrolabe-dsh-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
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
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib/index.js",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"deepseek-harness",
|
|
24
|
+
"dsh",
|
|
25
|
+
"astrolabe",
|
|
26
|
+
"hook",
|
|
27
|
+
"mcp"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT OR Apache-2.0",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/wojiushirencai/Astrolabe.git",
|
|
33
|
+
"directory": "dsh-plugin"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const REMIND_CMD = 'astrolabe hooks remind --client=zcode';
|
|
7
|
+
|
|
8
|
+
function detectZCode(home, env, { whichFn, existsFn }) {
|
|
9
|
+
const evidence = [];
|
|
10
|
+
if (whichFn('zcode', env)) evidence.push('zcode on PATH');
|
|
11
|
+
if (existsFn(path.join(home, '.zcode'))) evidence.push('~/.zcode/');
|
|
12
|
+
if (existsFn(path.join(home, '.zcode', 'cli'))) evidence.push('~/.zcode/cli/');
|
|
13
|
+
if (existsFn(path.join(home, '.zcode', 'cli', 'config.json'))) {
|
|
14
|
+
evidence.push('~/.zcode/cli/config.json');
|
|
15
|
+
}
|
|
16
|
+
return { detected: evidence.length > 0, evidence };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveZcodeConfigPath(home, existsFn) {
|
|
20
|
+
// Prefer documented ~/.zcode/cli/config.json (HOOKS.md + z.ai docs).
|
|
21
|
+
// Some forks also use setting.json; only fall back when that alone exists.
|
|
22
|
+
const configJson = path.join(home, '.zcode', 'cli', 'config.json');
|
|
23
|
+
const settingJson = path.join(home, '.zcode', 'cli', 'setting.json');
|
|
24
|
+
if (existsFn(configJson)) return configJson;
|
|
25
|
+
if (existsFn(settingJson)) return settingJson;
|
|
26
|
+
return configJson;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function zcodeServerEntry(launch) {
|
|
30
|
+
return {
|
|
31
|
+
type: 'stdio',
|
|
32
|
+
command: launch.command,
|
|
33
|
+
args: launch.args,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function preToolUseEntry() {
|
|
38
|
+
return {
|
|
39
|
+
matcher: '*',
|
|
40
|
+
hooks: [
|
|
41
|
+
{
|
|
42
|
+
type: 'command',
|
|
43
|
+
command: REMIND_CMD,
|
|
44
|
+
timeout: 10,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function entryHasRemind(entry) {
|
|
51
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
52
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
53
|
+
return hooks.some(
|
|
54
|
+
(h) =>
|
|
55
|
+
h &&
|
|
56
|
+
typeof h.command === 'string' &&
|
|
57
|
+
h.command.includes('astrolabe hooks remind --client=zcode'),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Merge mcp.servers.astrolabe and (optional) hooks.events.PreToolUse into
|
|
63
|
+
* ~/.zcode/cli/config.json. Never writes SessionEnd; never hangs cleanup on Stop.
|
|
64
|
+
*/
|
|
65
|
+
function mergeZcodeConfigFile(
|
|
66
|
+
filePath,
|
|
67
|
+
launch,
|
|
68
|
+
{ create = true, withHooks = false, existsFn = fs.existsSync } = {},
|
|
69
|
+
) {
|
|
70
|
+
let created = false;
|
|
71
|
+
let doc = {};
|
|
72
|
+
if (existsFn(filePath)) {
|
|
73
|
+
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
|
74
|
+
doc = JSON.parse(raw);
|
|
75
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
76
|
+
throw new Error(`${filePath} root must be a JSON object`);
|
|
77
|
+
}
|
|
78
|
+
} else if (!create) {
|
|
79
|
+
return { skipped: true, reason: `missing ${filePath} (will not create)` };
|
|
80
|
+
} else {
|
|
81
|
+
created = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!doc.mcp || typeof doc.mcp !== 'object' || Array.isArray(doc.mcp)) {
|
|
85
|
+
doc.mcp = {};
|
|
86
|
+
}
|
|
87
|
+
if (!doc.mcp.servers || typeof doc.mcp.servers !== 'object' || Array.isArray(doc.mcp.servers)) {
|
|
88
|
+
doc.mcp.servers = {};
|
|
89
|
+
}
|
|
90
|
+
const hadServer = Object.prototype.hasOwnProperty.call(doc.mcp.servers, 'astrolabe');
|
|
91
|
+
doc.mcp.servers.astrolabe = zcodeServerEntry(launch);
|
|
92
|
+
|
|
93
|
+
let hooksReplaced = false;
|
|
94
|
+
if (withHooks) {
|
|
95
|
+
if (!doc.hooks || typeof doc.hooks !== 'object' || Array.isArray(doc.hooks)) {
|
|
96
|
+
doc.hooks = {};
|
|
97
|
+
}
|
|
98
|
+
doc.hooks.enabled = true;
|
|
99
|
+
if (!doc.hooks.events || typeof doc.hooks.events !== 'object' || Array.isArray(doc.hooks.events)) {
|
|
100
|
+
doc.hooks.events = {};
|
|
101
|
+
}
|
|
102
|
+
// Hard rule: never SessionEnd (ZCode rejects unknown events).
|
|
103
|
+
if (Object.prototype.hasOwnProperty.call(doc.hooks.events, 'SessionEnd')) {
|
|
104
|
+
delete doc.hooks.events.SessionEnd;
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(doc.hooks.events.PreToolUse)) {
|
|
107
|
+
doc.hooks.events.PreToolUse = [];
|
|
108
|
+
}
|
|
109
|
+
const idx = doc.hooks.events.PreToolUse.findIndex(entryHasRemind);
|
|
110
|
+
const next = preToolUseEntry();
|
|
111
|
+
if (idx >= 0) {
|
|
112
|
+
doc.hooks.events.PreToolUse[idx] = next;
|
|
113
|
+
hooksReplaced = true;
|
|
114
|
+
} else {
|
|
115
|
+
doc.hooks.events.PreToolUse.push(next);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
filePath,
|
|
121
|
+
doc,
|
|
122
|
+
created,
|
|
123
|
+
replaced: hadServer || hooksReplaced,
|
|
124
|
+
entry: doc.mcp.servers.astrolabe,
|
|
125
|
+
withHooks,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
REMIND_CMD,
|
|
131
|
+
detectZCode,
|
|
132
|
+
resolveZcodeConfigPath,
|
|
133
|
+
zcodeServerEntry,
|
|
134
|
+
mergeZcodeConfigFile,
|
|
135
|
+
};
|