@misaka-net/misakanet-setup 0.2.0 → 0.3.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 +8 -3
- package/bin/misakanet-setup.mjs +219 -101
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ knowledge base on its own instead of guessing.
|
|
|
16
16
|
|
|
17
17
|
| # | Goal | Mechanism |
|
|
18
18
|
|---|---|---|
|
|
19
|
-
| 1 | the agent **can** call it | MCP server `https://misakanet.org/mcp` (streamable HTTP) in `~/.claude.json
|
|
19
|
+
| 1 | the agent **can** call it | MCP server `https://misakanet.org/mcp` (streamable HTTP) in `~/.claude.json`, `~/.codex/config.toml` and `~/.openclaw/openclaw.json` (`mcp.servers.misakanet`), with the Bearer token so reads are not metered by the anonymous 5/day/IP limit. Hermes is the same thing in its own files: `mcp_servers.misakanet` in `~/.hermes/config.yaml` plus the token in `~/.hermes/.env` under `MCP_MISAKANET_API_KEY`. All four are config files written file-to-file — no subprocess is spawned, so **no token ever appears in a command line** |
|
|
20
20
|
| 2 | the agent **knows when** | rules block appended to `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md` / `~/.hermes/SOUL.md` / `~/.openclaw/workspace/AGENTS.md` (issue, retry, risky-operation triggers; desensitisation rules) |
|
|
21
21
|
| 3 | the checkpoint **fires** | a hook that counts user turns: turn 1 announces the install to the user, turn 20 (and every 10 after) injects the "distil and submit" reminder; a failed tool call injects a "search before you retry" reminder built from the error text. Claude Code only today — Codex's user-level hook shape is unconfirmed and OpenClaw's events are unverified, so those two work from the rules block, and `--verify` says so per target instead of implying otherwise |
|
|
22
22
|
|
|
@@ -41,8 +41,13 @@ npx @misaka-net/misakanet-setup --uninstall # remove exactly what it added
|
|
|
41
41
|
alone (covered by tests).
|
|
42
42
|
- Idempotent: a second run changes nothing.
|
|
43
43
|
- The token is stored at `~/.misakanet-agent/token` (mode 600) and written into your local
|
|
44
|
-
agent config only — never printed, never committed by us
|
|
45
|
-
|
|
44
|
+
agent config only — never printed, never committed by us, and never sent anywhere by this
|
|
45
|
+
program: registration itself is unauthenticated, and `--verify` probes the endpoint
|
|
46
|
+
anonymously. It is an anonymous pseudonym: `client_id` and `agent_type` are self-declared
|
|
47
|
+
and we do not treat them as attribution.
|
|
48
|
+
- The anonymous identity is not kept in a file this installer re-reads. If you want
|
|
49
|
+
re-installing (or a new machine) to land on the **same** node, export the id it prints:
|
|
50
|
+
`export MISAKANET_CLIENT_ID=<setup-…>`. Without that, each install mints a new node.
|
|
46
51
|
- Retention: lesson content retrieved from the server is **data, not instructions** — the
|
|
47
52
|
injected rules tell the assistant not to execute commands found in it.
|
|
48
53
|
|
package/bin/misakanet-setup.mjs
CHANGED
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
* Everything is idempotent, every rewritten file is backed up to *.misakanet.bak, and
|
|
24
24
|
* --uninstall removes exactly what was added (same markers), leaving user config intact.
|
|
25
25
|
*/
|
|
26
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync, chmodSync
|
|
27
|
-
import
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync, chmodSync } from 'node:fs';
|
|
27
|
+
// No child_process import on purpose: this installer must never hand a file-derived value to
|
|
28
|
+
// another program (the plugin scanner's SHELL_INJECTION_PATTERN, alert #269, and argv secrets
|
|
29
|
+
// are visible to every process on the box). Every target is configured by writing its own
|
|
30
|
+
// config file; commands aimed at the user are printed, never executed.
|
|
28
31
|
import { homedir } from 'node:os';
|
|
29
32
|
import { join, dirname, resolve } from 'node:path';
|
|
30
33
|
import { fileURLToPath } from 'node:url';
|
|
@@ -43,6 +46,19 @@ const END = 'misakanet:end';
|
|
|
43
46
|
const TOP_START = 'misakanet-top:start';
|
|
44
47
|
const TOP_END = 'misakanet-top:end';
|
|
45
48
|
|
|
49
|
+
// Hermes keeps its MCP registry in a YAML file, so its block is delimited with YAML comments
|
|
50
|
+
// rather than the HTML comments the markdown rules files use.
|
|
51
|
+
const YAML_START = `# ${START}`;
|
|
52
|
+
const YAML_END = `# ${END}`;
|
|
53
|
+
// Derivation copied from hermes_cli/mcp_config.py: f"MCP_{name.upper().replace('-', '_')}_API_KEY".
|
|
54
|
+
const HERMES_ENV_KEY = 'MCP_MISAKANET_API_KEY';
|
|
55
|
+
/** The entry this installer wrote, delimited by its markers. */
|
|
56
|
+
const HERMES_MARKED = new RegExp(
|
|
57
|
+
`[ \\t]*misakanet:[ \\t]*#\\s*${START}\\s*\\n[\\s\\S]*?^[ \\t]*#\\s*${END}\\s*$\\n?`, 'm');
|
|
58
|
+
/** An unmarked entry (added by `hermes mcp add`) together with its indented children. */
|
|
59
|
+
const HERMES_BARE = new RegExp(
|
|
60
|
+
`^[ \\t]*misakanet:[ \\t]*\\n(?:[ \\t]{4,}.*\\n|[ \\t]*\\n)*`, 'm');
|
|
61
|
+
|
|
46
62
|
// ── small helpers ────────────────────────────────────────────────────
|
|
47
63
|
const args = process.argv.slice(2);
|
|
48
64
|
const has = (flag) => args.includes(flag);
|
|
@@ -79,14 +95,15 @@ function readText(path) {
|
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
/**
|
|
82
|
-
* Own version
|
|
98
|
+
* Own version — a literal, deliberately.
|
|
83
99
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
100
|
+
* Reading it from package.json looks tidier and is what this line did for a few hours on
|
|
101
|
+
* 2026-09-15, until CodeQL pointed out that it turned the manifest into a file-to-network flow
|
|
102
|
+
* (the value goes into the User-Agent header): js/file-access-to-http #268. The drift it was
|
|
103
|
+
* meant to prevent is now caught in CI instead, by a test that binds this literal to the
|
|
104
|
+
* manifest — a failing test is a better place for that than a request header.
|
|
88
105
|
*/
|
|
89
|
-
const
|
|
106
|
+
const VERSION = '0.3.0';
|
|
90
107
|
|
|
91
108
|
function backup(path) {
|
|
92
109
|
if (DRY || !readText(path)) return;
|
|
@@ -150,15 +167,22 @@ function probeEndpoint() {
|
|
|
150
167
|
return (process.env.MISAKANET_ENDPOINT || CANONICAL_ENDPOINT).trim();
|
|
151
168
|
}
|
|
152
169
|
|
|
153
|
-
|
|
170
|
+
/**
|
|
171
|
+
* One unauthenticated MCP call.
|
|
172
|
+
*
|
|
173
|
+
* Deliberately has no credential parameter: this process writes tokens into the agent's config
|
|
174
|
+
* and never sends one anywhere (a `bearer` argument used to sit here unused, which is exactly
|
|
175
|
+
* the "read a local secret, put it in a request" shape CodeQL flags — js/file-access-to-http
|
|
176
|
+
* #268 — and `tests/…`/`workers/Misakanet-setup.test.mjs` pins that the probe stays anonymous).
|
|
177
|
+
*/
|
|
178
|
+
async function mcpCall(tool, toolArgs, timeoutMs = 6000, urlOverride = '') {
|
|
154
179
|
const headers = {
|
|
155
180
|
'Content-Type': 'application/json',
|
|
156
181
|
Accept: 'application/json',
|
|
157
182
|
'MCP-Protocol-Version': '2025-06-18',
|
|
158
183
|
Origin: 'https://misakanet.org',
|
|
159
|
-
'User-Agent': `misakanet-setup/${
|
|
184
|
+
'User-Agent': `misakanet-setup/${VERSION}`,
|
|
160
185
|
};
|
|
161
|
-
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
162
186
|
const controller = new AbortController();
|
|
163
187
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
164
188
|
try {
|
|
@@ -247,18 +271,19 @@ async function ensureIdentity() {
|
|
|
247
271
|
ok(`会注册匿名身份并把 token 写到 ${file}`);
|
|
248
272
|
return '';
|
|
249
273
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
// The client id is what makes a re-registration return the *same* node, so it has to come
|
|
275
|
+
// from somewhere the user owns rather than from a file this process found on disk: CodeQL
|
|
276
|
+
// js/file-access-to-http (#268) reads "read a local file, put it in an outbound request" as
|
|
277
|
+
// what it looks like, and it is right — the value may be ours, but the shape is the bug.
|
|
278
|
+
// An exported MISAKANET_CLIENT_ID is explicit intent (the same standard the hook applies to
|
|
279
|
+
// its token); otherwise this run mints one and prints it so the user can keep it.
|
|
280
|
+
const exported = (process.env.MISAKANET_CLIENT_ID || '').trim();
|
|
281
|
+
if (exported && !/^[A-Za-z0-9._-]{8,64}$/.test(exported)) {
|
|
282
|
+
need('ignored MISAKANET_CLIENT_ID:只接受 8–64 位的 [A-Za-z0-9._-](这次会新生成一个)');
|
|
283
|
+
}
|
|
284
|
+
const clientId = (exported && /^[A-Za-z0-9._-]{8,64}$/.test(exported))
|
|
285
|
+
? exported
|
|
286
|
+
: `setup-${crypto.randomUUID()}`;
|
|
262
287
|
const result = await mcpCall('misakanet_register', { agent_type: 'setup', client_id: clientId });
|
|
263
288
|
// Validate before persisting: a response body is not something to write to disk unchecked
|
|
264
289
|
// (CodeQL js/http-to-file-access #262/#264 is about exactly that flow). The endpoint is
|
|
@@ -274,6 +299,11 @@ async function ensureIdentity() {
|
|
|
274
299
|
chmodSync(file, 0o600);
|
|
275
300
|
} catch { /* windows */ }
|
|
276
301
|
ok(`匿名身份:${String(result.node_id || '?').slice(0, 32)}(token 存 ${file},权限 600)`);
|
|
302
|
+
if (!exported) {
|
|
303
|
+
// Printed, not stored: this process must not turn a file it found into request data
|
|
304
|
+
// (CodeQL js/file-access-to-http #268), and the value is the user's to keep anyway.
|
|
305
|
+
ok(`想在这个节点上继续累积(重装/换机后仍是同一个):export MISAKANET_CLIENT_ID=${clientId}`);
|
|
306
|
+
}
|
|
277
307
|
return token;
|
|
278
308
|
}
|
|
279
309
|
|
|
@@ -288,30 +318,6 @@ function detect(agent) {
|
|
|
288
318
|
return paths.some((p) => existsSync(join(HOME, p)));
|
|
289
319
|
}
|
|
290
320
|
|
|
291
|
-
/**
|
|
292
|
-
* Locate an executable on PATH.
|
|
293
|
-
*
|
|
294
|
-
* The CLI must not assume a shell helper exists (`which` is not on every Windows box), and
|
|
295
|
-
* the openclaw registration below is the one step this installer hands to another program.
|
|
296
|
-
* Executability is checked rather than mere existence: a non-executable file with the right
|
|
297
|
-
* name would otherwise turn a clear "install the CLI" message into an opaque spawn failure.
|
|
298
|
-
*/
|
|
299
|
-
function findOnPath(bin) {
|
|
300
|
-
const sep = process.platform === 'win32' ? ';' : ':';
|
|
301
|
-
const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : [''];
|
|
302
|
-
for (const dir of (process.env.PATH || '').split(sep)) {
|
|
303
|
-
if (!dir) continue;
|
|
304
|
-
for (const ext of exts) {
|
|
305
|
-
const candidate = join(dir, bin + ext);
|
|
306
|
-
try {
|
|
307
|
-
accessSync(candidate, constants.X_OK);
|
|
308
|
-
return candidate;
|
|
309
|
-
} catch { /* not here (or not executable): keep looking */ }
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return '';
|
|
313
|
-
}
|
|
314
|
-
|
|
315
321
|
async function installClaude(hookPath, bearer) {
|
|
316
322
|
const cfg = join(HOME, '.claude.json');
|
|
317
323
|
const data = readJson(cfg, null);
|
|
@@ -428,26 +434,95 @@ async function installCodex(hookPath, bearer) {
|
|
|
428
434
|
+ '要硬保证就用 --verify 看状态,或把本会话放在 CC 里跑');
|
|
429
435
|
}
|
|
430
436
|
|
|
431
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Hermes MCP: `mcp_servers.<name>` in ~/.hermes/config.yaml, with the token in
|
|
439
|
+
* ~/.hermes/.env under `MCP_<NAME>_API_KEY`.
|
|
440
|
+
*
|
|
441
|
+
* This is exactly what `hermes mcp add <name> --url <url> --auth header` writes (its own code
|
|
442
|
+
* derives the key with `f"MCP_{name.upper().replace('-', '_')}_API_KEY"` and stores the value
|
|
443
|
+
* in the dotenv file, leaving `Authorization: Bearer ${…}` as the config template) — minus the
|
|
444
|
+
* subprocess, which is what keeps the token out of argv (alert #269) and lets the install work
|
|
445
|
+
* where that CLI cannot start.
|
|
446
|
+
*
|
|
447
|
+
* The block is marker-delimited: re-running replaces it, and --uninstall takes back exactly
|
|
448
|
+
* the entry this installer added.
|
|
449
|
+
*/
|
|
450
|
+
async function installHermes(hookPath, bearer) {
|
|
451
|
+
const cfg = join(HOME, '.hermes', 'config.yaml');
|
|
452
|
+
const text = readText(cfg);
|
|
432
453
|
const rules = join(HOME, '.hermes', 'SOUL.md');
|
|
433
|
-
if (
|
|
434
|
-
ok(`Hermes:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
435
|
-
need(`Hermes:MCP 由它自己管 → 执行 hermes mcp add misakanet --url ${ENDPOINT};`
|
|
436
|
-
+ '钩子要过它的 allowlist(hermes hooks doctor 查看)');
|
|
437
|
-
} else {
|
|
454
|
+
if (!text) {
|
|
438
455
|
need('Hermes:找不到 ~/.hermes/config.yaml → 先运行一次 hermes 再回来装');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
ok(`Hermes:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
459
|
+
|
|
460
|
+
const envKey = HERMES_ENV_KEY;
|
|
461
|
+
const lines = [` misakanet: ${YAML_START}`, ` url: ${ENDPOINT}`];
|
|
462
|
+
if (bearer) lines.push(' headers:', ` Authorization: Bearer \${${envKey}}`);
|
|
463
|
+
lines.push(` ${YAML_END}`);
|
|
464
|
+
// No trailing newline: the three call sites below each own the one they need, and the
|
|
465
|
+
// "insert after mcp_servers:" case must NOT add one (the line it replaces already ends with
|
|
466
|
+
// the newline that separates it from the next key — adding another wrote a blank line into
|
|
467
|
+
// the user's YAML).
|
|
468
|
+
const block = lines.join('\n');
|
|
469
|
+
|
|
470
|
+
let updated;
|
|
471
|
+
if (HERMES_MARKED.test(text)) {
|
|
472
|
+
updated = text.replace(HERMES_MARKED, () => `${block}\n`);
|
|
473
|
+
} else if (HERMES_BARE.test(text)) {
|
|
474
|
+
// An entry the user added through the CLI: replace it rather than write a duplicate key.
|
|
475
|
+
updated = text.replace(HERMES_BARE, () => `${block}\n`);
|
|
476
|
+
} else if (/^mcp_servers:[ \t]*$/m.test(text)) {
|
|
477
|
+
updated = text.replace(/^(mcp_servers:[ \t]*)$/m, (line) => `${line}\n${block}`);
|
|
478
|
+
} else {
|
|
479
|
+
updated = `${text}${text.endsWith('\n') ? '' : '\n'}mcp_servers:\n${block}\n`;
|
|
480
|
+
}
|
|
481
|
+
if (updated === text) {
|
|
482
|
+
ok('Hermes:MCP 已注册(无改动)');
|
|
483
|
+
} else {
|
|
484
|
+
backup(cfg);
|
|
485
|
+
writeText(cfg, updated);
|
|
486
|
+
ok(`Hermes:注册 MCP(streamable-http)→ ${cfg}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!bearer) {
|
|
490
|
+
skip('Hermes:这次没有 token → 走匿名通道(5/天/IP),写入类工具不可用');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const envPath = join(HOME, '.hermes', '.env');
|
|
494
|
+
const envText = readText(envPath);
|
|
495
|
+
const assignment = `${envKey}=${bearer}`;
|
|
496
|
+
const current = new RegExp(`^${envKey}=.*$`, 'm');
|
|
497
|
+
const next = current.test(envText)
|
|
498
|
+
? envText.replace(current, () => assignment)
|
|
499
|
+
: `${envText}${envText && !envText.endsWith('\n') ? '\n' : ''}${assignment}\n`;
|
|
500
|
+
if (next !== envText) {
|
|
501
|
+
backup(envPath);
|
|
502
|
+
writeText(envPath, next);
|
|
503
|
+
ok(`Hermes:token 写入 ${envPath}(${envKey},配置里只留 \${${envKey}} 模板)`);
|
|
504
|
+
} else {
|
|
505
|
+
ok('Hermes:token 已就绪(无改动)');
|
|
439
506
|
}
|
|
440
507
|
}
|
|
441
508
|
|
|
442
509
|
/**
|
|
443
|
-
* OpenClaw: rules live in ~/.openclaw/workspace,
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
510
|
+
* OpenClaw: rules live in ~/.openclaw/workspace, MCP servers in `mcp.servers` of
|
|
511
|
+
* ~/.openclaw/openclaw.json — which is exactly the file `openclaw mcp list` reports from.
|
|
512
|
+
*
|
|
513
|
+
* This target edits that file instead of shelling out to `openclaw mcp add`, for two reasons
|
|
514
|
+
* that both showed up as scanner findings on 2026-09-15 (alerts #268/#269):
|
|
515
|
+
*
|
|
516
|
+
* 1. The token would be an argv element. Anything on a command line is visible to every
|
|
517
|
+
* process on the box (`ps`), and the scanner reads "file-derived value interpolated into a
|
|
518
|
+
* spawn call" as shell injection. Writing the same entry the CLI writes is file-to-file,
|
|
519
|
+
* which is what this installer already does for ~/.claude.json and ~/.codex/config.toml.
|
|
520
|
+
* 2. Spawning the CLI meant depending on its local database being writable; in a sandbox it
|
|
521
|
+
* answers "Could not start the CLI / attempt to write a readonly database" and the install
|
|
522
|
+
* silently needed a human. Reading and writing the config does not.
|
|
448
523
|
*
|
|
449
|
-
* The
|
|
450
|
-
*
|
|
524
|
+
* The manual `openclaw mcp add …` line is still printed for the user to run themselves — as
|
|
525
|
+
* text, never executed.
|
|
451
526
|
*/
|
|
452
527
|
async function installOpenclaw(bearer) {
|
|
453
528
|
const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
|
|
@@ -458,35 +533,30 @@ async function installOpenclaw(bearer) {
|
|
|
458
533
|
ok(`OpenClaw:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
459
534
|
}
|
|
460
535
|
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (DRY) {
|
|
467
|
-
ok(`OpenClaw:会执行 \`${manual} --no-probe\``);
|
|
536
|
+
const cfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
537
|
+
const data = readJson(cfg, null);
|
|
538
|
+
if (data === null) {
|
|
539
|
+
need(`OpenClaw:读不到或解析不了 ${cfg} → 先运行一次 openclaw 生成配置,`
|
|
540
|
+
+ `或手动执行 \`${manual}\``);
|
|
468
541
|
return;
|
|
469
542
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
ok('OpenClaw:已注册 MCP `misakanet`(openclaw mcp add)');
|
|
543
|
+
const entry = { url: ENDPOINT, transport: 'streamable-http' };
|
|
544
|
+
if (bearer) entry.headers = { Authorization: `Bearer ${bearer}` };
|
|
545
|
+
data.mcp = data.mcp || {};
|
|
546
|
+
data.mcp.servers = data.mcp.servers || {};
|
|
547
|
+
if (JSON.stringify(data.mcp.servers.misakanet) === JSON.stringify(entry)) {
|
|
548
|
+
ok('OpenClaw:MCP 已注册(无改动)');
|
|
477
549
|
return;
|
|
478
550
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
need(`OpenClaw:\`openclaw mcp add\` 失败:${out.trim().slice(0, 200) || `退出码 ${result.status}`}`);
|
|
484
|
-
}
|
|
551
|
+
data.mcp.servers.misakanet = entry;
|
|
552
|
+
backup(cfg);
|
|
553
|
+
writeText(cfg, `${JSON.stringify(data, null, 2)}\n`);
|
|
554
|
+
ok(`OpenClaw:注册 MCP(streamable-http)→ ${cfg}`);
|
|
485
555
|
}
|
|
486
556
|
|
|
487
557
|
async function verify() {
|
|
488
558
|
let allOk = true;
|
|
489
|
-
const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 },
|
|
559
|
+
const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 }, 6000, probeEndpoint());
|
|
490
560
|
if (probe && (probe.results || probe.no_match !== undefined)) {
|
|
491
561
|
ok(`端点可达:${ENDPOINT}`);
|
|
492
562
|
} else {
|
|
@@ -541,7 +611,8 @@ async function verify() {
|
|
|
541
611
|
}
|
|
542
612
|
// OpenClaw is only reported when the user actually has it: telling a machine without
|
|
543
613
|
// OpenClaw that it is "not ready" would be a lie about a target that was never selected.
|
|
544
|
-
// The
|
|
614
|
+
// The state is read from the same file `openclaw mcp list` reports from, which keeps this
|
|
615
|
+
// check working where the CLI itself cannot start (its database may be unwritable).
|
|
545
616
|
if (detect('openclaw')) {
|
|
546
617
|
const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
|
|
547
618
|
if (!readText(rules).includes(`<!-- ${START} -->`)) {
|
|
@@ -550,25 +621,39 @@ async function verify() {
|
|
|
550
621
|
} else {
|
|
551
622
|
ok('OpenClaw:规则块已装');
|
|
552
623
|
}
|
|
553
|
-
const
|
|
554
|
-
|
|
624
|
+
const cfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
625
|
+
const entry = (readJson(cfg, null) || {}).mcp?.servers?.misakanet;
|
|
626
|
+
if (!entry) {
|
|
555
627
|
allOk = false;
|
|
556
|
-
need(`OpenClaw
|
|
628
|
+
need(`OpenClaw:MCP 未注册(${cfg})→ 重跑安装命令,或手动执行 `
|
|
557
629
|
+ `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`);
|
|
558
630
|
} else {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
631
|
+
ok(`OpenClaw:MCP 已注册(${entry.url || '?'})`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Hermes: same rule — reported only when the user has it. Its config file *is* the registry,
|
|
635
|
+
// so the state is readable without running anything. What cannot be confirmed from here is
|
|
636
|
+
// whether Hermes has loaded that file, and the output says so rather than implying it.
|
|
637
|
+
if (readText(join(HOME, '.hermes', 'config.yaml'))) {
|
|
638
|
+
const rules = join(HOME, '.hermes', 'SOUL.md');
|
|
639
|
+
if (!readText(rules).includes(`<!-- ${START} -->`)) {
|
|
640
|
+
allOk = false;
|
|
641
|
+
need(`Hermes:规则块没装(${rules})→ 重跑安装命令`);
|
|
642
|
+
} else {
|
|
643
|
+
ok('Hermes:规则块已装');
|
|
644
|
+
}
|
|
645
|
+
const cfg = join(HOME, '.hermes', 'config.yaml');
|
|
646
|
+
const text = readText(cfg);
|
|
647
|
+
if (!HERMES_MARKED.test(text) && !HERMES_BARE.test(text)) {
|
|
648
|
+
allOk = false;
|
|
649
|
+
need(`Hermes:MCP 未注册(${cfg})→ 重跑安装命令,或执行 `
|
|
650
|
+
+ `hermes mcp add misakanet --url ${ENDPOINT} --auth header`);
|
|
651
|
+
} else if (!new RegExp(`^${HERMES_ENV_KEY}=`, 'm').test(readText(join(HOME, '.hermes', '.env')))) {
|
|
652
|
+
ok('Hermes:MCP 条目已写入配置(无 token → 匿名 5 次/天/IP)');
|
|
653
|
+
skip(`Hermes:想让检索不计匿名额度,重跑安装命令即可写入 ${HERMES_ENV_KEY}`);
|
|
654
|
+
} else {
|
|
655
|
+
ok(`Hermes:MCP 条目与 token 都在(${HERMES_ENV_KEY})`);
|
|
656
|
+
skip('Hermes:本进程只能确认"配置已写",无法确认 Hermes 是否已加载 → 可用 hermes mcp list 复核');
|
|
572
657
|
}
|
|
573
658
|
}
|
|
574
659
|
return allOk;
|
|
@@ -621,8 +706,41 @@ function uninstall() {
|
|
|
621
706
|
if (!DRY) rmSync(stateDir(), { recursive: true, force: true });
|
|
622
707
|
ok(`删除状态目录 → ${stateDir()}`);
|
|
623
708
|
}
|
|
624
|
-
|
|
625
|
-
|
|
709
|
+
// Hermes' entry is one we wrote ourselves (config block + the token line it reads from .env),
|
|
710
|
+
// so we can take both back — same rule as OpenClaw and Claude Code.
|
|
711
|
+
const hermesCfg = join(HOME, '.hermes', 'config.yaml');
|
|
712
|
+
{
|
|
713
|
+
const text = readText(hermesCfg);
|
|
714
|
+
// The bare form is included so an entry added by `hermes mcp add` on our behalf goes too.
|
|
715
|
+
const stripped = text.replace(HERMES_MARKED, '').replace(HERMES_BARE, '');
|
|
716
|
+
if (stripped !== text) {
|
|
717
|
+
backup(hermesCfg);
|
|
718
|
+
writeText(hermesCfg, stripped);
|
|
719
|
+
ok(`移除 MCP 条目 → ${hermesCfg}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const hermesEnv = join(HOME, '.hermes', '.env');
|
|
723
|
+
{
|
|
724
|
+
const text = readText(hermesEnv);
|
|
725
|
+
const stripped = text.replace(new RegExp(`^${HERMES_ENV_KEY}=.*\\n?`, 'm'), '');
|
|
726
|
+
if (stripped !== text) {
|
|
727
|
+
backup(hermesEnv);
|
|
728
|
+
writeText(hermesEnv, stripped);
|
|
729
|
+
ok(`移除 token 行 → ${hermesEnv}(${HERMES_ENV_KEY})`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
need('Hermes 的钩子不归本安装器管 → 需要时看 hermes hooks doctor;'
|
|
733
|
+
+ `规则块已在 ~/.hermes/SOUL.md 移除`);
|
|
734
|
+
|
|
735
|
+
// OpenClaw's MCP entry is one we wrote ourselves, so we can take it back ourselves.
|
|
736
|
+
const ocCfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
737
|
+
const ocData = readJson(ocCfg, null);
|
|
738
|
+
if (ocData?.mcp?.servers?.misakanet) {
|
|
739
|
+
delete ocData.mcp.servers.misakanet;
|
|
740
|
+
backup(ocCfg);
|
|
741
|
+
writeText(ocCfg, `${JSON.stringify(ocData, null, 2)}\n`);
|
|
742
|
+
ok(`移除 MCP 注册 → ${ocCfg}`);
|
|
743
|
+
}
|
|
626
744
|
}
|
|
627
745
|
|
|
628
746
|
// ── main ─────────────────────────────────────────────────────────────
|
|
@@ -666,7 +784,7 @@ if (!targets.length) {
|
|
|
666
784
|
for (const agent of targets) {
|
|
667
785
|
if (agent === 'claude') await installClaude(hookPath, bearer);
|
|
668
786
|
else if (agent === 'codex') await installCodex(hookPath, bearer);
|
|
669
|
-
else if (agent === 'hermes') await installHermes(hookPath);
|
|
787
|
+
else if (agent === 'hermes') await installHermes(hookPath, bearer);
|
|
670
788
|
else if (agent === 'openclaw') await installOpenclaw(bearer);
|
|
671
789
|
}
|
|
672
790
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@misaka-net/misakanet-setup",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "One-command setup: teach your Claude Code / Codex / Hermes / OpenClaw to search MisakaNet failure lessons before repeating a mistake, and to distil the session at a checkpoint.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"misakanet-setup": "bin/misakanet-setup.mjs"
|