@misaka-net/misakanet-setup 0.5.5 → 0.5.6
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/bin/misakanet-setup.mjs +263 -43
- 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`, `~/.codex/config.toml` and `~/.openclaw/openclaw.json` (`mcp.servers.misakanet`), with the Bearer token
|
|
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 when registration succeeds — reads need no token (anonymous reads are unmetered since 2026-09-18); the token is what opens the **write** path. Every entry also carries the self-declared context hints `X-MisakaNet-Client` / `-Agent` / `-Os` / `-Version`, which the endpoint records on the read's analytics row and never treats as identity (`codewhale` is the one exception: its server entry has no custom-header field). 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**. For Claude Code only, the five **read-only** tools are also added to `permissions.allow` — "can call it" otherwise still means a permission prompt on the very first search, so `0.5.3` pre-allows them (`--verify` reports it, `--report` prints `permissions:`); `write_lesson` is deliberately **not** pre-allowed, because a tool that writes should ask |
|
|
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). Wording is **imperative first** ("必须先调 misakanet_search") rather than descriptive, because a field test showed a weaker model reads a descriptive block as background and never calls the tool |
|
|
21
21
|
| 4 | *(opt-in)* you **hear** it | `--voice` adds a `PostToolUse` hook that plays the cue the server asked for (`lesson-found` / `failure-warning` / `connect-success` / `pair-success`). Off by default; mute with `MISAKANET_VOICE=0`; see `docs/integrations/mcp-voice-hooks.md` |
|
|
22
22
|
| 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 |
|
package/bin/misakanet-setup.mjs
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* undone by the other.
|
|
17
17
|
*
|
|
18
18
|
* Three things, all required, or the install is theatre:
|
|
19
|
-
* 1. the agent CAN call it → MCP server entry (with the token
|
|
20
|
-
*
|
|
19
|
+
* 1. the agent CAN call it → MCP server entry (with the token when we have one, plus the
|
|
20
|
+
* self-declared context hints; reads are unlimited either way)
|
|
21
21
|
* 2. the agent KNOWS when to call → rules block in its own rules file
|
|
22
22
|
* 3. the checkpoint FIRES → a hook, because "summarise every 20 turns" is dead
|
|
23
23
|
* text: agents do not keep counters
|
|
@@ -39,7 +39,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFi
|
|
|
39
39
|
// package.json since 0.4 and had never been executed on the version it names.
|
|
40
40
|
import { randomUUID } from 'node:crypto';
|
|
41
41
|
import { homedir } from 'node:os';
|
|
42
|
-
import { delimiter, join, dirname, resolve } from 'node:path';
|
|
42
|
+
import { basename, delimiter, join, dirname, resolve } from 'node:path';
|
|
43
43
|
import { fileURLToPath } from 'node:url';
|
|
44
44
|
|
|
45
45
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -78,6 +78,25 @@ const valueOf = (flag, dflt) => {
|
|
|
78
78
|
};
|
|
79
79
|
const only = valueOf('--only', '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
80
80
|
const DRY = has('--dry-run');
|
|
81
|
+
/**
|
|
82
|
+
* Permission tiers (2026-09-18).
|
|
83
|
+
*
|
|
84
|
+
* The installer does three different kinds of thing, and they do not need the same permission:
|
|
85
|
+
*
|
|
86
|
+
* ① register an MCP endpoint — the user's own agent config; the same privilege as adding
|
|
87
|
+
* any MCP server;
|
|
88
|
+
* ② write a rules block — changes the agent's behaviour ("search before retrying"),
|
|
89
|
+
* and for Hermes that means editing `~/.hermes/SOUL.md`, its
|
|
90
|
+
* *identity* file;
|
|
91
|
+
* ③ install hooks + state — acts automatically on every turn.
|
|
92
|
+
*
|
|
93
|
+
* Asking for all three in one command is why a machine with an approval process can only answer
|
|
94
|
+
* "no" (issue #1753, 2026-09-18): an operator willing to approve ① has no way to say it. `--mcp-only`
|
|
95
|
+
* is that way to say it — ① only, and the run says so in its own report instead of looking partial.
|
|
96
|
+
*/
|
|
97
|
+
const MCP_ONLY = has('--mcp-only');
|
|
98
|
+
const LIST_WRITES = has('--list-writes');
|
|
99
|
+
|
|
81
100
|
/**
|
|
82
101
|
* `--silent` (issue #1784): the enterprise/MDM form. A GPO or Intune script pipes this program's
|
|
83
102
|
* stdout into a log file, where the ✓/· narration is noise (and names local paths).
|
|
@@ -128,7 +147,18 @@ function readText(path) {
|
|
|
128
147
|
* meant to prevent is now caught in CI instead, by a test that binds this literal to the
|
|
129
148
|
* manifest — a failing test is a better place for that than a request header.
|
|
130
149
|
*/
|
|
131
|
-
const VERSION = '0.5.
|
|
150
|
+
const VERSION = '0.5.6'
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The shape of a client id, in one place.
|
|
154
|
+
*
|
|
155
|
+
* It gates two different things: what the endpoint accepts as `client_id` on registration, and — since
|
|
156
|
+
* 2026-09-19 — what may be written into an assistant's config as `X-MisakaNet-Client`. The second use
|
|
157
|
+
* is why the shape matters here and not only at the endpoint: that value can come from a file we found
|
|
158
|
+
* on disk, and it is interpolated into a TOML inline table, a YAML mapping and JSON. A string matching
|
|
159
|
+
* this cannot contain a quote, a newline or a `:`, so it cannot break any of the three.
|
|
160
|
+
*/
|
|
161
|
+
const CLIENT_ID_SHAPE = /^[A-Za-z0-9._-]{8,64}$/
|
|
132
162
|
|
|
133
163
|
// ── argument surface ─────────────────────────────────────────────────
|
|
134
164
|
// Until 2026-09-17 no flag was validated at all: `--help` fell through to a *real install*
|
|
@@ -148,7 +178,9 @@ const FLAGS = [
|
|
|
148
178
|
['--uninstall', '移除本安装器写入的内容(保留 .misakanet.bak 备份)'],
|
|
149
179
|
['--upgrade', '与安装等价(覆盖安装即升级)'],
|
|
150
180
|
['--client-id <id>', '固定本机身份:同一个 id 永远拿回同一个 node(重装/换机也能延续)'],
|
|
151
|
-
['--
|
|
181
|
+
['--mcp-only', '只注册 MCP 端点,不改 agent 行为(不写规则块、不装钩子)——权限分级用'],
|
|
182
|
+
['--list-writes', '只打印会改哪些文件(文件 | 动作 | 撤销方式),不写任何东西'],
|
|
183
|
+
['--no-register', '不注册匿名节点(不写 token;读不受影响,写入类工具不可用)'],
|
|
152
184
|
['--voice', '打开语音/桌面通知(默认关)'],
|
|
153
185
|
['--help, -h', '打印这份帮助并退出'],
|
|
154
186
|
['--version', '打印版本并退出'],
|
|
@@ -174,6 +206,9 @@ if (has('--version')) {
|
|
|
174
206
|
process.exit(0);
|
|
175
207
|
}
|
|
176
208
|
|
|
209
|
+
/** What we would write, and how to take it back — printed by --list-writes, and used by the report. */
|
|
210
|
+
const WRITE_MANIFEST = [];
|
|
211
|
+
|
|
177
212
|
const KNOWN = new Set(FLAGS.flatMap(([flag]) => flag.split(/[,\s]/).filter((f) => f.startsWith('-'))));
|
|
178
213
|
{
|
|
179
214
|
const unknown = args.filter((a) => a.startsWith('-') && !KNOWN.has(a));
|
|
@@ -415,6 +450,45 @@ function stripBlock(path) {
|
|
|
415
450
|
|
|
416
451
|
const stateDir = () => join(HOME, '.misakanet-agent');
|
|
417
452
|
|
|
453
|
+
/**
|
|
454
|
+
* The headers every MCP entry we write carries.
|
|
455
|
+
*
|
|
456
|
+
* Two kinds, and the difference matters: `Authorization` is a credential (only when we have a token),
|
|
457
|
+
* while the other four are **self-declared hints** the service records on its analytics row — the
|
|
458
|
+
* stable pseudonym `X-MisakaNet-Client` (so a caller's history stays together without an account), the
|
|
459
|
+
* assistant name, the platform, and the installer version that wrote this. The endpoint treats them as
|
|
460
|
+
* hints and never as identity, which is what makes it acceptable to write them automatically
|
|
461
|
+
* (see AGENTS.md §3.3).
|
|
462
|
+
*
|
|
463
|
+
* Written only for agents whose config format supports custom headers; `--mcp-only` gets them too,
|
|
464
|
+
* because "the tool is callable" is exactly what tier ① means.
|
|
465
|
+
*/
|
|
466
|
+
function mcpHeaders(bearer, agent) {
|
|
467
|
+
const headers = {};
|
|
468
|
+
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
469
|
+
const clientId = declaredClientId();
|
|
470
|
+
if (clientId) headers['X-MisakaNet-Client'] = clientId;
|
|
471
|
+
headers['X-MisakaNet-Agent'] = agent;
|
|
472
|
+
headers['X-MisakaNet-Os'] = `${process.platform}/${process.arch}`.slice(0, 40);
|
|
473
|
+
headers['X-MisakaNet-Version'] = VERSION;
|
|
474
|
+
return headers;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The pseudonym to declare, or `''` when this machine has not said.
|
|
479
|
+
*
|
|
480
|
+
* Explicit intent beats a file we merely found, the same rule the register path applies. The file is
|
|
481
|
+
* `integrations/agent-autostart`'s (it keeps one there for the Python channel), so picking it up is
|
|
482
|
+
* continuity worth having — but it is data from outside this process on its way into three config
|
|
483
|
+
* syntaxes, so it is checked against `CLIENT_ID_SHAPE` first. A `client_id` that is not id-shaped is
|
|
484
|
+
* not a hint worth writing; it is noise that could close a quote or a mapping.
|
|
485
|
+
*/
|
|
486
|
+
function declaredClientId() {
|
|
487
|
+
if (knownClientId) return knownClientId;
|
|
488
|
+
const found = readText(join(stateDir(), 'client_id')).trim();
|
|
489
|
+
return CLIENT_ID_SHAPE.test(found) ? found : '';
|
|
490
|
+
}
|
|
491
|
+
|
|
418
492
|
const CANONICAL_ENDPOINT = 'https://misakanet.org/mcp';
|
|
419
493
|
|
|
420
494
|
/**
|
|
@@ -661,6 +735,16 @@ async function installHook() {
|
|
|
661
735
|
}
|
|
662
736
|
|
|
663
737
|
async function ensureIdentity() {
|
|
738
|
+
// The explicit intent is read first, because it decides the `X-MisakaNet-Client` hint on every run
|
|
739
|
+
// — including one that registers nothing (the token is already there) — so it cannot sit below the
|
|
740
|
+
// early returns. `--client-id` and `MISAKANET_CLIENT_ID` are the same intent.
|
|
741
|
+
const explicit = (valueOf('--client-id', '') || process.env.MISAKANET_CLIENT_ID || '').trim();
|
|
742
|
+
const usable = CLIENT_ID_SHAPE.test(explicit);
|
|
743
|
+
if (explicit && !usable) {
|
|
744
|
+
need('ignored --client-id / MISAKANET_CLIENT_ID:只接受 8–64 位的 [A-Za-z0-9._-](这次会新生成一个)');
|
|
745
|
+
}
|
|
746
|
+
if (usable) knownClientId = explicit;
|
|
747
|
+
|
|
664
748
|
const file = join(stateDir(), 'token');
|
|
665
749
|
let existing = '';
|
|
666
750
|
try {
|
|
@@ -685,13 +769,8 @@ async function ensureIdentity() {
|
|
|
685
769
|
// `--client-id` and `MISAKANET_CLIENT_ID` are the same explicit intent. The flag exists because
|
|
686
770
|
// the closing guidance told users to keep an identity and the only way to supply one was a shell
|
|
687
771
|
// export — an instruction a non-technical user cannot follow, and on Windows a different one.
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
need('ignored --client-id / MISAKANET_CLIENT_ID:只接受 8–64 位的 [A-Za-z0-9._-](这次会新生成一个)');
|
|
691
|
-
}
|
|
692
|
-
const clientId = (exported && /^[A-Za-z0-9._-]{8,64}$/.test(exported))
|
|
693
|
-
? exported
|
|
694
|
-
: `setup-${randomUUID()}`;
|
|
772
|
+
const clientId = usable ? explicit : `setup-${randomUUID()}`;
|
|
773
|
+
knownClientId = clientId;
|
|
695
774
|
const result = await mcpCall('misakanet_register', { agent_type: 'setup', client_id: clientId });
|
|
696
775
|
// Validate before persisting: a response body is not something to write to disk unchecked
|
|
697
776
|
// (CodeQL js/http-to-file-access #262/#264 is about exactly that flow). The endpoint is
|
|
@@ -699,18 +778,18 @@ async function ensureIdentity() {
|
|
|
699
778
|
// a visible, debuggable outcome instead of a silent 401 later.
|
|
700
779
|
const token = typeof result?.token === 'string' ? result.token.trim() : '';
|
|
701
780
|
if (!/^mcp_[A-Za-z0-9_-]{20,}$/.test(token)) {
|
|
702
|
-
// Say what it costs, not what went wrong internally: "凭据形状不对" is jargon
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
need('匿名注册没成功(多半是网络)→
|
|
706
|
-
+ '网络恢复后重跑本命令就能拿到 token
|
|
781
|
+
// Say what it costs, not what went wrong internally: "凭据形状不对" is jargon. Reading is not
|
|
782
|
+
// what is lost here (it needs no token and is not metered) — the write path is, and saying so
|
|
783
|
+
// plainly is the difference between a user retrying and a user concluding the tool is broken.
|
|
784
|
+
need('匿名注册没成功(多半是网络)→ 读课程不受影响,但写入类工具(write_lesson)现在用不了;'
|
|
785
|
+
+ '网络恢复后重跑本命令就能拿到 token');
|
|
707
786
|
return '';
|
|
708
787
|
}
|
|
709
788
|
// Created with mode 0600 rather than written and then chmodded: the old order left a window in
|
|
710
789
|
// which the token sat in a world-readable file on every single run.
|
|
711
790
|
writeText(file, token, { mode: 0o600 });
|
|
712
791
|
ok(`匿名身份:${String(result.node_id || '?').slice(0, 32)}(token 存 ${file},权限 600)`);
|
|
713
|
-
if (!
|
|
792
|
+
if (!usable) {
|
|
714
793
|
// Printed, not stored: this process must not turn a file it found into request data
|
|
715
794
|
// (CodeQL js/file-access-to-http #268), and the value is the user's to keep anyway.
|
|
716
795
|
// Kept out of the "done" list on purpose — it is something to *keep*, not something that
|
|
@@ -722,6 +801,18 @@ async function ensureIdentity() {
|
|
|
722
801
|
}
|
|
723
802
|
|
|
724
803
|
// ── per-agent install ────────────────────────────────────────────────
|
|
804
|
+
/** Is our MCP entry already in this agent's config? (read-only; used by the report's scope line) */
|
|
805
|
+
function mcpEntryPresent(agent) {
|
|
806
|
+
const text = {
|
|
807
|
+
claude: () => JSON.stringify(readJson(join(HOME, '.claude.json'), {})?.mcpServers?.misakanet || null),
|
|
808
|
+
codex: () => readText(join(HOME, '.codex', 'config.toml')),
|
|
809
|
+
hermes: () => readText(join(HOME, '.hermes', 'config.yaml')),
|
|
810
|
+
openclaw: () => readText(join(HOME, '.openclaw', 'openclaw.json')),
|
|
811
|
+
codewhale: () => readText(join(HOME, '.codewhale', 'mcp.json')),
|
|
812
|
+
}[agent]?.() || '';
|
|
813
|
+
return text.includes('misakanet') && text !== 'null';
|
|
814
|
+
}
|
|
815
|
+
|
|
725
816
|
function detect(agent) {
|
|
726
817
|
const paths = {
|
|
727
818
|
claude: ['.claude.json', '.claude'],
|
|
@@ -742,8 +833,7 @@ async function installClaude(hookPath, bearer) {
|
|
|
742
833
|
}
|
|
743
834
|
const doc = data || {};
|
|
744
835
|
doc.mcpServers = doc.mcpServers || {};
|
|
745
|
-
const entry = { type: 'http', url: ENDPOINT };
|
|
746
|
-
if (bearer) entry.headers = { Authorization: `Bearer ${bearer}` };
|
|
836
|
+
const entry = { type: 'http', url: ENDPOINT, headers: mcpHeaders(bearer, 'claude-code') };
|
|
747
837
|
if (sameJson(doc.mcpServers.misakanet, entry)) {
|
|
748
838
|
ok('Claude Code:MCP 已注册(无改动)');
|
|
749
839
|
} else {
|
|
@@ -754,8 +844,30 @@ async function installClaude(hookPath, bearer) {
|
|
|
754
844
|
}
|
|
755
845
|
|
|
756
846
|
const rules = join(HOME, '.claude', 'CLAUDE.md');
|
|
757
|
-
ok(`Claude Code:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
758
|
-
|
|
847
|
+
if (!MCP_ONLY) ok(`Claude Code:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
848
|
+
else skip('Claude Code:规则块未写(--mcp-only:只注册端点)');
|
|
849
|
+
|
|
850
|
+
if (MCP_ONLY) {
|
|
851
|
+
// Tier ① = "the tool can be called", and that includes the read-only grants: the comment below
|
|
852
|
+
// says it plainly — without them the first search is *denied*, so an operator who approved
|
|
853
|
+
// "add an MCP server" would be shipping a tool their users cannot use. The grants are therefore
|
|
854
|
+
// tier ①. What tier ③ adds is the hooks, and those are not written here.
|
|
855
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
856
|
+
const settings = readJson(settingsPath, {}) || {};
|
|
857
|
+
const allow = ((settings.permissions = settings.permissions || {}).allow =
|
|
858
|
+
Array.isArray(settings.permissions.allow) ? settings.permissions.allow : []);
|
|
859
|
+
let changed = false;
|
|
860
|
+
for (const tool of CLAUDE_ALLOWED_TOOLS) {
|
|
861
|
+
if (!allow.includes(tool)) { allow.push(tool); changed = true; }
|
|
862
|
+
}
|
|
863
|
+
if (changed) {
|
|
864
|
+
backup(settingsPath);
|
|
865
|
+
writeText(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
866
|
+
}
|
|
867
|
+
ok(`Claude Code:只读工具已放行(${CLAUDE_ALLOWED_TOOLS.length} 个${changed ? '' : ',无改动'})`);
|
|
868
|
+
skip('Claude Code:自动沉淀钩子未装(--mcp-only:只注册端点与工具放行)');
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
759
871
|
if (!hookPath) {
|
|
760
872
|
need('Claude Code:跳过了"自动沉淀"钩子(钩子文件没取到)');
|
|
761
873
|
return;
|
|
@@ -852,12 +964,17 @@ async function installClaude(hookPath, bearer) {
|
|
|
852
964
|
|
|
853
965
|
const codexTable = (bearer) => {
|
|
854
966
|
const lines = ['[mcp_servers.misakanet]', 'type = "streamable-http"', `url = "${ENDPOINT}"`];
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
967
|
+
// http_headers, not bearer_token_env_var: the env var needs the user to export it, and this user
|
|
968
|
+
// will not. All the headers go into the ONE inline table — a second `http_headers` line is a
|
|
969
|
+
// duplicate key, and TOML refuses the whole file for it. (Bare TOML keys may contain `-`, which is
|
|
970
|
+
// why the hint names need no quoting.)
|
|
971
|
+
const headers = Object.entries(mcpHeaders(bearer, 'codex'))
|
|
972
|
+
.map(([key, value]) => `${key} = "${value}"`).join(', ');
|
|
973
|
+
lines.push(`http_headers = { ${headers} }`);
|
|
974
|
+
if (!bearer) {
|
|
975
|
+
// Only worth saying when it is true. Since 2026-09-18 anonymous reads are not metered — what a
|
|
976
|
+
// token buys is the write path, not more reads.
|
|
977
|
+
lines.push('# 没有 token:读不限次数,写入类工具(write_lesson)不可用');
|
|
861
978
|
}
|
|
862
979
|
return `${lines.join('\n')}\n`;
|
|
863
980
|
};
|
|
@@ -909,7 +1026,8 @@ async function installCodex(hookPath, bearer) {
|
|
|
909
1026
|
}
|
|
910
1027
|
|
|
911
1028
|
const rules = join(HOME, '.codex', 'AGENTS.md');
|
|
912
|
-
ok(`Codex:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
1029
|
+
if (!MCP_ONLY) ok(`Codex:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
1030
|
+
else skip('Codex:规则块未写(--mcp-only:只注册端点)');
|
|
913
1031
|
// Verified on codex-cli 0.154.0 (2026-09-15), so this is no longer a "could not
|
|
914
1032
|
// confirm" note: `codex mcp list` shows misakanet enabled with the Bearer token,
|
|
915
1033
|
// `codex doctor` reports config.toml parse ok + 1 streamable_http server + 0
|
|
@@ -1036,11 +1154,18 @@ async function installHermes(hookPath, bearer) {
|
|
|
1036
1154
|
need('Hermes:找不到 ~/.hermes/config.yaml → 先运行一次 hermes 再回来装');
|
|
1037
1155
|
return;
|
|
1038
1156
|
}
|
|
1039
|
-
ok(`Hermes:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
1157
|
+
if (!MCP_ONLY) ok(`Hermes:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
1158
|
+
else skip('Hermes:规则块未写(--mcp-only:只注册端点;SOUL.md 是它的人格文件)');
|
|
1040
1159
|
|
|
1041
1160
|
const envKey = HERMES_ENV_KEY;
|
|
1042
1161
|
const lines = [` misakanet: ${YAML_START}`, ` url: ${ENDPOINT}`];
|
|
1043
|
-
|
|
1162
|
+
lines.push(' headers:');
|
|
1163
|
+
if (bearer) lines.push(` Authorization: Bearer \${${envKey}}`);
|
|
1164
|
+
// Hermes reads its headers from YAML, so these are literals rather than env templates: they are
|
|
1165
|
+
// hints, not secrets (a token would have to go through `.env` like the Authorization line above).
|
|
1166
|
+
for (const [key, value] of Object.entries(mcpHeaders('', 'hermes'))) {
|
|
1167
|
+
lines.push(` ${key}: ${value}`);
|
|
1168
|
+
}
|
|
1044
1169
|
lines.push(` ${YAML_END}`);
|
|
1045
1170
|
// No trailing newline: the three call sites below each own the one they need, and the
|
|
1046
1171
|
// "insert after mcp_servers:" case must NOT add one (the line it replaces already ends with
|
|
@@ -1068,7 +1193,7 @@ async function installHermes(hookPath, bearer) {
|
|
|
1068
1193
|
}
|
|
1069
1194
|
|
|
1070
1195
|
if (!bearer) {
|
|
1071
|
-
skip('Hermes:这次没有 token →
|
|
1196
|
+
skip('Hermes:这次没有 token → 读仍然不限次数,写入类工具不可用');
|
|
1072
1197
|
return;
|
|
1073
1198
|
}
|
|
1074
1199
|
const envPath = join(HOME, '.hermes', '.env');
|
|
@@ -1117,6 +1242,11 @@ async function installHermes(hookPath, bearer) {
|
|
|
1117
1242
|
* The token is the one thing codewhale will not take inline: `bearer_token_env_var` names
|
|
1118
1243
|
* an environment variable, so "install once" for this agent ends with the user exporting
|
|
1119
1244
|
* `MISAKANET_TOKEN` once. That is stated rather than silently half-installed.
|
|
1245
|
+
*
|
|
1246
|
+
* This is also the one agent that gets no `X-MisakaNet-*` hints: its server entry has a fixed
|
|
1247
|
+
* field set (`command`/`args`/`env`/`url`/timeouts/`bearer_token_env_var`) with no custom-header
|
|
1248
|
+
* field, and inventing one would risk the file being rejected outright. Hints are optional by
|
|
1249
|
+
* design — a call without them is still a normal anonymous read.
|
|
1120
1250
|
*/
|
|
1121
1251
|
function codewhaleProjects() {
|
|
1122
1252
|
const text = readText(join(HOME, '.codewhale', 'config.toml'));
|
|
@@ -1166,7 +1296,9 @@ async function installCodewhale(bearer) {
|
|
|
1166
1296
|
const projects = codewhaleProjects();
|
|
1167
1297
|
for (const project of projects) {
|
|
1168
1298
|
if (!existsSync(project)) continue;
|
|
1169
|
-
|
|
1299
|
+
if (!MCP_ONLY) {
|
|
1300
|
+
ok(`codewhale:规则块 ${injectBlock(join(project, 'AGENTS.md'), PROMPT_BLOCK)} → ${join(project, 'AGENTS.md')}`);
|
|
1301
|
+
} else skip('codewhale:规则块未写(--mcp-only)');
|
|
1170
1302
|
}
|
|
1171
1303
|
if (!projects.length) {
|
|
1172
1304
|
need('codewhale:没有受信任的项目目录(config.toml 里没有 trust_level = "trusted")→ '
|
|
@@ -1183,7 +1315,8 @@ async function installOpenclaw(bearer) {
|
|
|
1183
1315
|
if (!existsSync(workspace)) {
|
|
1184
1316
|
need(`OpenClaw:找不到 workspace(${workspace})→ 先运行一次 openclaw 生成它`);
|
|
1185
1317
|
} else {
|
|
1186
|
-
ok(`OpenClaw:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
1318
|
+
if (!MCP_ONLY) ok(`OpenClaw:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
1319
|
+
else skip('OpenClaw:规则块未写(--mcp-only)');
|
|
1187
1320
|
}
|
|
1188
1321
|
|
|
1189
1322
|
const cfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
@@ -1193,8 +1326,8 @@ async function installOpenclaw(bearer) {
|
|
|
1193
1326
|
+ `或手动执行 \`${manual}\``);
|
|
1194
1327
|
return;
|
|
1195
1328
|
}
|
|
1196
|
-
const entry = { url: ENDPOINT, transport: 'streamable-http'
|
|
1197
|
-
|
|
1329
|
+
const entry = { url: ENDPOINT, transport: 'streamable-http',
|
|
1330
|
+
headers: mcpHeaders(bearer, 'openclaw') };
|
|
1198
1331
|
data.mcp = data.mcp || {};
|
|
1199
1332
|
data.mcp.servers = data.mcp.servers || {};
|
|
1200
1333
|
if (sameJson(data.mcp.servers.misakanet, entry)) {
|
|
@@ -1251,9 +1384,9 @@ async function verify() {
|
|
|
1251
1384
|
tokenPresent = '';
|
|
1252
1385
|
}
|
|
1253
1386
|
if (tokenPresent) {
|
|
1254
|
-
ok('写入通道:token
|
|
1387
|
+
ok('写入通道:token 已就绪(write_lesson / preflight 可用)');
|
|
1255
1388
|
} else {
|
|
1256
|
-
skip('写入通道:无 token
|
|
1389
|
+
skip('写入通道:无 token(读不限次数、只读完全可用,写入类工具不可用)');
|
|
1257
1390
|
}
|
|
1258
1391
|
const hookPath = join(stateDir(), 'hook.mjs');
|
|
1259
1392
|
let hookPresent = false;
|
|
@@ -1331,8 +1464,8 @@ async function verify() {
|
|
|
1331
1464
|
need(`Hermes:MCP 未注册(${cfg})→ 重跑安装命令,或执行 `
|
|
1332
1465
|
+ `hermes mcp add misakanet --url ${ENDPOINT} --auth header`);
|
|
1333
1466
|
} else if (!new RegExp(`^${HERMES_ENV_KEY}=`, 'm').test(readText(join(HOME, '.hermes', '.env')))) {
|
|
1334
|
-
ok('Hermes:MCP 条目已写入配置(无 token →
|
|
1335
|
-
skip(`Hermes
|
|
1467
|
+
ok('Hermes:MCP 条目已写入配置(无 token → 读不限次数,写入类工具不可用)');
|
|
1468
|
+
skip(`Hermes:想让写入类工具可用(write_lesson),重跑安装命令即可写入 ${HERMES_ENV_KEY}`);
|
|
1336
1469
|
} else {
|
|
1337
1470
|
ok(`Hermes:MCP 条目与 token 都在(${HERMES_ENV_KEY})`);
|
|
1338
1471
|
skip('Hermes:本进程只能确认"配置已写",无法确认 Hermes 是否已加载 → 可用 hermes mcp list 复核');
|
|
@@ -1664,6 +1797,11 @@ function reportValues(allOk) {
|
|
|
1664
1797
|
['token', token ? 'present' : 'absent'],
|
|
1665
1798
|
['permissions', permissions],
|
|
1666
1799
|
['hook', existsSync(join(stateDir(), 'hook.mjs')) ? 'present' : 'absent'],
|
|
1800
|
+
// `mcp-only` = the endpoint is registered but the hooks are not, which is a different machine
|
|
1801
|
+
// from "nothing installed" and must not be counted as one (#1753's T1-lite rows).
|
|
1802
|
+
['install-scope', existsSync(join(stateDir(), 'hook.mjs'))
|
|
1803
|
+
? 'full'
|
|
1804
|
+
: (AGENTS.some((agent) => detect(agent) && mcpEntryPresent(agent)) ? 'mcp-only' : 'none')],
|
|
1667
1805
|
['voice', voiceStatus()],
|
|
1668
1806
|
['open-items', manual.length],
|
|
1669
1807
|
// Redacted here, once, so neither encoder can print an unredacted message by accident.
|
|
@@ -1779,7 +1917,14 @@ if (mode !== 'report' && !SILENT) {
|
|
|
1779
1917
|
|
|
1780
1918
|
if (mode === 'uninstall') {
|
|
1781
1919
|
uninstall();
|
|
1782
|
-
|
|
1920
|
+
if (MCP_ONLY) {
|
|
1921
|
+
// The honest failure mode of tier ①: the tool is callable, but nothing tells the agent (or the
|
|
1922
|
+
// user) to call it — the rules block and the first-turn announcement live in tiers ②/③. Printed
|
|
1923
|
+
// as a "manual step" so it survives --silent (a deployment log keeps the `!` lines).
|
|
1924
|
+
need('只注册了端点:你的 agent 不会自己想到去查 —— 需要你自己说「先查 MisakaNet」,'
|
|
1925
|
+
+ '或者请运营方批准 ②③ 后重跑同一条命令(去掉 --mcp-only)');
|
|
1926
|
+
}
|
|
1927
|
+
say(render());
|
|
1783
1928
|
if (!SILENT) {
|
|
1784
1929
|
console.log('\n已移除本安装器写入的内容(规则块、MCP 注册、钩子、状态目录)。');
|
|
1785
1930
|
console.log(`备份保留在 <被改过的文件>.misakanet.bak(例如 ${join(HOME, '.claude', 'CLAUDE.md')}.misakanet.bak)`
|
|
@@ -1866,12 +2011,80 @@ const targets = (only.length ? only : AGENTS).filter((a) => {
|
|
|
1866
2011
|
let installed = 0;
|
|
1867
2012
|
// Set when a stable client id was minted: shown to the user at the end (see ensureIdentity).
|
|
1868
2013
|
let clientIdHint = '';
|
|
2014
|
+
/**
|
|
2015
|
+
* The pseudonym this run knows the machine by — an explicit `--client-id` / `MISAKANET_CLIENT_ID`, or
|
|
2016
|
+
* the one this run minted. This is what `X-MisakaNet-Client` carries.
|
|
2017
|
+
*
|
|
2018
|
+
* Deliberately separate from `clientIdHint`, which exists only for the closing "keep this number"
|
|
2019
|
+
* guidance: a user who supplied an id must not be told to keep it, but their config still has to say
|
|
2020
|
+
* who they are — otherwise the hint would be missing for exactly the users who took the trouble to
|
|
2021
|
+
* have an identity at all.
|
|
2022
|
+
*/
|
|
2023
|
+
let knownClientId = '';
|
|
2024
|
+
/**
|
|
2025
|
+
* `--list-writes`: the manifest an operator needs to approve, or to undo by hand.
|
|
2026
|
+
*
|
|
2027
|
+
* `--dry-run` already says which files it would touch; this adds the two things an approval process
|
|
2028
|
+
* asks for — *what* each write does (new file / append a marked block / add a config key) and *how to
|
|
2029
|
+
* take it back* — and it is machine-readable so a ticket can carry it.
|
|
2030
|
+
*/
|
|
2031
|
+
if (LIST_WRITES) {
|
|
2032
|
+
// Every row carries its **tier**, which is the point: an operator approving "add an MCP server"
|
|
2033
|
+
// (①) is not approving "change how the agent behaves" (② rules block, ③ automatic hooks). Files
|
|
2034
|
+
// already exist → we append or update keys; missing → we create them.
|
|
2035
|
+
// Compare base names, not separators: a `/settings.json$` regex silently classifies nothing on
|
|
2036
|
+
// Windows (all three windows legs went red on the first CI run of this change), and a manifest
|
|
2037
|
+
// that hides a tier is worse than no manifest.
|
|
2038
|
+
const tierOf = (path) => {
|
|
2039
|
+
const base = basename(path);
|
|
2040
|
+
if (base === 'CLAUDE.md' || base === 'AGENTS.md' || base === 'SOUL.md') return 2;
|
|
2041
|
+
if (path.includes('.misakanet-agent')) return 3; // our own state dir
|
|
2042
|
+
// `settings.json` carries both the read-only grants (①) and the hooks (③); the caller adds the
|
|
2043
|
+
// second row only when the hooks are in scope.
|
|
2044
|
+
if (base === 'settings.json' && path.includes('.claude')) return 1;
|
|
2045
|
+
return 1; // an MCP entry in the agent's config
|
|
2046
|
+
};
|
|
2047
|
+
const rows = new Map();
|
|
2048
|
+
const add = (path, action, tier) => {
|
|
2049
|
+
const key = `${tier}|${path}`;
|
|
2050
|
+
if (!rows.has(key)) rows.set(key, { path: redact(path), action, tier });
|
|
2051
|
+
};
|
|
2052
|
+
for (const agent of AGENTS) {
|
|
2053
|
+
if (!detect(agent)) continue;
|
|
2054
|
+
for (const path of (AGENT_WRITE_PATHS[agent] || (() => []))(HOME)) {
|
|
2055
|
+
add(path, existsSync(path) ? 'append-or-update' : 'create', tierOf(path));
|
|
2056
|
+
if (!MCP_ONLY && basename(path) === 'settings.json' && path.includes('.claude')) {
|
|
2057
|
+
add(path, 'append-or-update(钩子)', 3);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
add(join(stateDir(), 'token'), 'create (0600)', 1);
|
|
2062
|
+
if (!MCP_ONLY) add(join(stateDir(), 'hook.mjs'), 'create', 3);
|
|
2063
|
+
|
|
2064
|
+
const tiers = { 1: '① 注册 MCP 端点', 2: '② 写规则块(改行为)', 3: '③ 装钩子(每轮自动执行)' };
|
|
2065
|
+
const list = [...rows.values()].filter((row) => (MCP_ONLY ? row.tier === 1 : true))
|
|
2066
|
+
.sort((a, b) => a.tier - b.tier || a.path.localeCompare(b.path));
|
|
2067
|
+
for (const row of list) {
|
|
2068
|
+
console.log(`tier${row.tier}\t${tiers[row.tier]}\t${row.path}\t${row.action}`);
|
|
2069
|
+
}
|
|
2070
|
+
const shown = new Set(list.map((row) => row.tier));
|
|
2071
|
+
if (MCP_ONLY) {
|
|
2072
|
+
console.log('# --mcp-only:只申请 ①。规则块(②)与钩子(③)不会写入,'
|
|
2073
|
+
+ '需要时由运营方另行批准后重跑不带 --mcp-only 的同一条命令。');
|
|
2074
|
+
} else {
|
|
2075
|
+
console.log(`# 本次申请 ${shown.size} 级权限(${[...shown].sort().map((t) => tiers[t]).join(' · ')});`
|
|
2076
|
+
+ '只想批 ① 就加 --mcp-only。');
|
|
2077
|
+
}
|
|
2078
|
+
console.log('# 这次没有写任何东西。撤销:npx @misaka-net/misakanet-setup --uninstall');
|
|
2079
|
+
process.exit(0);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1869
2082
|
try {
|
|
1870
2083
|
if (!targets.length) {
|
|
1871
2084
|
need('没检测到 Claude Code / Codex / Hermes 的配置目录 → 请先打开一次你要用的那个助手,再回来运行本命令');
|
|
1872
2085
|
} else {
|
|
1873
|
-
const hookPath = await installHook();
|
|
1874
|
-
stampVersion();
|
|
2086
|
+
const hookPath = MCP_ONLY ? null : await installHook();
|
|
2087
|
+
if (!MCP_ONLY) stampVersion();
|
|
1875
2088
|
const bearer = has('--no-register') ? '' : await ensureIdentity();
|
|
1876
2089
|
for (const agent of targets) {
|
|
1877
2090
|
// One target failing must not skip the rest: an uncaught throw used to abort the loop, so
|
|
@@ -1909,6 +2122,13 @@ try {
|
|
|
1909
2122
|
// the exact failures in it on a dirty one — which is the point (#1784). The machine-readable form
|
|
1910
2123
|
// of the same run is `--report-json`, not this text.
|
|
1911
2124
|
say(render());
|
|
2125
|
+
if (MCP_ONLY && !SILENT) {
|
|
2126
|
+
// The honest failure mode of tier ①, said where the user will read it: they are not installing a
|
|
2127
|
+
// behaviour. The rules block (②) and the first-turn announcement, which is injected by the hook
|
|
2128
|
+
// (③), are exactly what makes an agent look things up on its own.
|
|
2129
|
+
console.log('\n⚠️ --mcp-only:只注册了端点与只读工具放行 —— **你的 agent 不会自己想到去查**。'
|
|
2130
|
+
+ '\n 需要你自己说「先查 MisakaNet」,或请运营方批准 ②③ 后重跑同一条命令(去掉 --mcp-only)。');
|
|
2131
|
+
}
|
|
1912
2132
|
if (!SILENT) {
|
|
1913
2133
|
console.log(`
|
|
1914
2134
|
接下来:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@misaka-net/misakanet-setup",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "One-command setup: teach your Claude Code / Codex / Hermes / OpenClaw / codewhale 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"
|