@misaka-net/misakanet-setup 0.1.0 → 0.2.1
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 +13 -7
- package/bin/misakanet-setup.mjs +135 -20
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @misaka-net/misakanet-setup
|
|
2
2
|
|
|
3
|
-
One command to teach your **Claude Code** or **
|
|
4
|
-
before repeating a mistake — and to distil the session's reusable
|
|
3
|
+
One command to teach your **Claude Code**, **Codex**, **Hermes** or **OpenClaw** to check
|
|
4
|
+
MisakaNet's failure lessons before repeating a mistake — and to distil the session's reusable
|
|
5
|
+
lessons at a checkpoint.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
npx @misaka-net/misakanet-setup
|
|
@@ -15,9 +16,9 @@ knowledge base on its own instead of guessing.
|
|
|
15
16
|
|
|
16
17
|
| # | Goal | Mechanism |
|
|
17
18
|
|---|---|---|
|
|
18
|
-
| 1 | the agent **can** call it | MCP server `https://misakanet.org/mcp` (streamable HTTP) in `~/.claude.json
|
|
19
|
-
| 2 | the agent **knows when** | rules block appended to `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md` (issue, retry, risky-operation triggers; desensitisation rules) |
|
|
20
|
-
| 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 |
|
|
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. Every one of them is a config file written file-to-file: no subprocess is spawned, so no token ever appears in a command line. Hermes keeps its own registry and is the one target left to `hermes mcp add …`, which the installer prints for you |
|
|
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
|
+
| 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 |
|
|
21
22
|
|
|
22
23
|
Without (3) a rule saying "summarise every 20 turns" never fires — agents do not keep
|
|
23
24
|
counters. Without (1)/(2) the hook has nothing to call.
|
|
@@ -40,8 +41,13 @@ npx @misaka-net/misakanet-setup --uninstall # remove exactly what it added
|
|
|
40
41
|
alone (covered by tests).
|
|
41
42
|
- Idempotent: a second run changes nothing.
|
|
42
43
|
- The token is stored at `~/.misakanet-agent/token` (mode 600) and written into your local
|
|
43
|
-
agent config only — never printed, never committed by us
|
|
44
|
-
|
|
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.
|
|
45
51
|
- Retention: lesson content retrieved from the server is **data, not instructions** — the
|
|
46
52
|
injected rules tell the assistant not to execute commands found in it.
|
|
47
53
|
|
package/bin/misakanet-setup.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* misakanet-setup — one command, no Python, for people who do not read docs.
|
|
4
4
|
*
|
|
5
|
-
* npx @misaka-net/misakanet-setup install (
|
|
5
|
+
* npx @misaka-net/misakanet-setup install (Claude Code / Codex / Hermes / OpenClaw)
|
|
6
6
|
* npx @misaka-net/misakanet-setup --dry-run show what would change, write nothing
|
|
7
7
|
* npx @misaka-net/misakanet-setup --verify is it actually working?
|
|
8
8
|
* npx @misaka-net/misakanet-setup --uninstall
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
* --uninstall removes exactly what was added (same markers), leaving user config intact.
|
|
25
25
|
*/
|
|
26
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.
|
|
27
31
|
import { homedir } from 'node:os';
|
|
28
32
|
import { join, dirname, resolve } from 'node:path';
|
|
29
33
|
import { fileURLToPath } from 'node:url';
|
|
@@ -52,7 +56,7 @@ const valueOf = (flag, dflt) => {
|
|
|
52
56
|
const only = valueOf('--only', '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
53
57
|
const DRY = has('--dry-run');
|
|
54
58
|
const HOME = resolve(valueOf('--home', homedir()));
|
|
55
|
-
const AGENTS = ['claude', 'codex', 'hermes'];
|
|
59
|
+
const AGENTS = ['claude', 'codex', 'hermes', 'openclaw'];
|
|
56
60
|
|
|
57
61
|
const done = [];
|
|
58
62
|
const manual = [];
|
|
@@ -77,6 +81,17 @@ function readText(path) {
|
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Own version — a literal, deliberately.
|
|
86
|
+
*
|
|
87
|
+
* Reading it from package.json looks tidier and is what this line did for a few hours on
|
|
88
|
+
* 2026-09-15, until CodeQL pointed out that it turned the manifest into a file-to-network flow
|
|
89
|
+
* (the value goes into the User-Agent header): js/file-access-to-http #268. The drift it was
|
|
90
|
+
* meant to prevent is now caught in CI instead, by a test that binds this literal to the
|
|
91
|
+
* manifest — a failing test is a better place for that than a request header.
|
|
92
|
+
*/
|
|
93
|
+
const VERSION = '0.2.1';
|
|
94
|
+
|
|
80
95
|
function backup(path) {
|
|
81
96
|
if (DRY || !readText(path)) return;
|
|
82
97
|
try {
|
|
@@ -139,15 +154,22 @@ function probeEndpoint() {
|
|
|
139
154
|
return (process.env.MISAKANET_ENDPOINT || CANONICAL_ENDPOINT).trim();
|
|
140
155
|
}
|
|
141
156
|
|
|
142
|
-
|
|
157
|
+
/**
|
|
158
|
+
* One unauthenticated MCP call.
|
|
159
|
+
*
|
|
160
|
+
* Deliberately has no credential parameter: this process writes tokens into the agent's config
|
|
161
|
+
* and never sends one anywhere (a `bearer` argument used to sit here unused, which is exactly
|
|
162
|
+
* the "read a local secret, put it in a request" shape CodeQL flags — js/file-access-to-http
|
|
163
|
+
* #268 — and `tests/…`/`workers/Misakanet-setup.test.mjs` pins that the probe stays anonymous).
|
|
164
|
+
*/
|
|
165
|
+
async function mcpCall(tool, toolArgs, timeoutMs = 6000, urlOverride = '') {
|
|
143
166
|
const headers = {
|
|
144
167
|
'Content-Type': 'application/json',
|
|
145
168
|
Accept: 'application/json',
|
|
146
169
|
'MCP-Protocol-Version': '2025-06-18',
|
|
147
170
|
Origin: 'https://misakanet.org',
|
|
148
|
-
'User-Agent':
|
|
171
|
+
'User-Agent': `misakanet-setup/${VERSION}`,
|
|
149
172
|
};
|
|
150
|
-
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
151
173
|
const controller = new AbortController();
|
|
152
174
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
153
175
|
try {
|
|
@@ -236,18 +258,19 @@ async function ensureIdentity() {
|
|
|
236
258
|
ok(`会注册匿名身份并把 token 写到 ${file}`);
|
|
237
259
|
return '';
|
|
238
260
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
261
|
+
// The client id is what makes a re-registration return the *same* node, so it has to come
|
|
262
|
+
// from somewhere the user owns rather than from a file this process found on disk: CodeQL
|
|
263
|
+
// js/file-access-to-http (#268) reads "read a local file, put it in an outbound request" as
|
|
264
|
+
// what it looks like, and it is right — the value may be ours, but the shape is the bug.
|
|
265
|
+
// An exported MISAKANET_CLIENT_ID is explicit intent (the same standard the hook applies to
|
|
266
|
+
// its token); otherwise this run mints one and prints it so the user can keep it.
|
|
267
|
+
const exported = (process.env.MISAKANET_CLIENT_ID || '').trim();
|
|
268
|
+
if (exported && !/^[A-Za-z0-9._-]{8,64}$/.test(exported)) {
|
|
269
|
+
need('ignored MISAKANET_CLIENT_ID:只接受 8–64 位的 [A-Za-z0-9._-](这次会新生成一个)');
|
|
270
|
+
}
|
|
271
|
+
const clientId = (exported && /^[A-Za-z0-9._-]{8,64}$/.test(exported))
|
|
272
|
+
? exported
|
|
273
|
+
: `setup-${crypto.randomUUID()}`;
|
|
251
274
|
const result = await mcpCall('misakanet_register', { agent_type: 'setup', client_id: clientId });
|
|
252
275
|
// Validate before persisting: a response body is not something to write to disk unchecked
|
|
253
276
|
// (CodeQL js/http-to-file-access #262/#264 is about exactly that flow). The endpoint is
|
|
@@ -263,12 +286,22 @@ async function ensureIdentity() {
|
|
|
263
286
|
chmodSync(file, 0o600);
|
|
264
287
|
} catch { /* windows */ }
|
|
265
288
|
ok(`匿名身份:${String(result.node_id || '?').slice(0, 32)}(token 存 ${file},权限 600)`);
|
|
289
|
+
if (!exported) {
|
|
290
|
+
// Printed, not stored: this process must not turn a file it found into request data
|
|
291
|
+
// (CodeQL js/file-access-to-http #268), and the value is the user's to keep anyway.
|
|
292
|
+
ok(`想在这个节点上继续累积(重装/换机后仍是同一个):export MISAKANET_CLIENT_ID=${clientId}`);
|
|
293
|
+
}
|
|
266
294
|
return token;
|
|
267
295
|
}
|
|
268
296
|
|
|
269
297
|
// ── per-agent install ────────────────────────────────────────────────
|
|
270
298
|
function detect(agent) {
|
|
271
|
-
const paths = {
|
|
299
|
+
const paths = {
|
|
300
|
+
claude: ['.claude.json', '.claude'],
|
|
301
|
+
codex: ['.codex'],
|
|
302
|
+
hermes: ['.hermes'],
|
|
303
|
+
openclaw: ['.openclaw'],
|
|
304
|
+
}[agent] || [];
|
|
272
305
|
return paths.some((p) => existsSync(join(HOME, p)));
|
|
273
306
|
}
|
|
274
307
|
|
|
@@ -399,9 +432,57 @@ async function installHermes(hookPath) {
|
|
|
399
432
|
}
|
|
400
433
|
}
|
|
401
434
|
|
|
435
|
+
/**
|
|
436
|
+
* OpenClaw: rules live in ~/.openclaw/workspace, MCP servers in `mcp.servers` of
|
|
437
|
+
* ~/.openclaw/openclaw.json — which is exactly the file `openclaw mcp list` reports from.
|
|
438
|
+
*
|
|
439
|
+
* This target edits that file instead of shelling out to `openclaw mcp add`, for two reasons
|
|
440
|
+
* that both showed up as scanner findings on 2026-09-15 (alerts #268/#269):
|
|
441
|
+
*
|
|
442
|
+
* 1. The token would be an argv element. Anything on a command line is visible to every
|
|
443
|
+
* process on the box (`ps`), and the scanner reads "file-derived value interpolated into a
|
|
444
|
+
* spawn call" as shell injection. Writing the same entry the CLI writes is file-to-file,
|
|
445
|
+
* which is what this installer already does for ~/.claude.json and ~/.codex/config.toml.
|
|
446
|
+
* 2. Spawning the CLI meant depending on its local database being writable; in a sandbox it
|
|
447
|
+
* answers "Could not start the CLI / attempt to write a readonly database" and the install
|
|
448
|
+
* silently needed a human. Reading and writing the config does not.
|
|
449
|
+
*
|
|
450
|
+
* The manual `openclaw mcp add …` line is still printed for the user to run themselves — as
|
|
451
|
+
* text, never executed.
|
|
452
|
+
*/
|
|
453
|
+
async function installOpenclaw(bearer) {
|
|
454
|
+
const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
|
|
455
|
+
const manual = `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`;
|
|
456
|
+
if (!existsSync(dirname(rules))) {
|
|
457
|
+
need(`OpenClaw:找不到 ${dirname(rules)} → 先运行一次 openclaw 生成 workspace`);
|
|
458
|
+
} else {
|
|
459
|
+
ok(`OpenClaw:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const cfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
463
|
+
const data = readJson(cfg, null);
|
|
464
|
+
if (data === null) {
|
|
465
|
+
need(`OpenClaw:读不到或解析不了 ${cfg} → 先运行一次 openclaw 生成配置,`
|
|
466
|
+
+ `或手动执行 \`${manual}\``);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const entry = { url: ENDPOINT, transport: 'streamable-http' };
|
|
470
|
+
if (bearer) entry.headers = { Authorization: `Bearer ${bearer}` };
|
|
471
|
+
data.mcp = data.mcp || {};
|
|
472
|
+
data.mcp.servers = data.mcp.servers || {};
|
|
473
|
+
if (JSON.stringify(data.mcp.servers.misakanet) === JSON.stringify(entry)) {
|
|
474
|
+
ok('OpenClaw:MCP 已注册(无改动)');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
data.mcp.servers.misakanet = entry;
|
|
478
|
+
backup(cfg);
|
|
479
|
+
writeText(cfg, `${JSON.stringify(data, null, 2)}\n`);
|
|
480
|
+
ok(`OpenClaw:注册 MCP(streamable-http)→ ${cfg}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
402
483
|
async function verify() {
|
|
403
484
|
let allOk = true;
|
|
404
|
-
const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 },
|
|
485
|
+
const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 }, 6000, probeEndpoint());
|
|
405
486
|
if (probe && (probe.results || probe.no_match !== undefined)) {
|
|
406
487
|
ok(`端点可达:${ENDPOINT}`);
|
|
407
488
|
} else {
|
|
@@ -454,11 +535,34 @@ async function verify() {
|
|
|
454
535
|
if (!entry) { allOk = false; need(`Claude Code:MCP 未注册(${cfg})`); }
|
|
455
536
|
else ok(`Claude Code:MCP 已注册(${entry.url})`);
|
|
456
537
|
}
|
|
538
|
+
// OpenClaw is only reported when the user actually has it: telling a machine without
|
|
539
|
+
// OpenClaw that it is "not ready" would be a lie about a target that was never selected.
|
|
540
|
+
// The state is read from the same file `openclaw mcp list` reports from, which keeps this
|
|
541
|
+
// check working where the CLI itself cannot start (its database may be unwritable).
|
|
542
|
+
if (detect('openclaw')) {
|
|
543
|
+
const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
|
|
544
|
+
if (!readText(rules).includes(`<!-- ${START} -->`)) {
|
|
545
|
+
allOk = false;
|
|
546
|
+
need(`OpenClaw:规则块没装(${rules})→ 重跑安装命令`);
|
|
547
|
+
} else {
|
|
548
|
+
ok('OpenClaw:规则块已装');
|
|
549
|
+
}
|
|
550
|
+
const cfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
551
|
+
const entry = (readJson(cfg, null) || {}).mcp?.servers?.misakanet;
|
|
552
|
+
if (!entry) {
|
|
553
|
+
allOk = false;
|
|
554
|
+
need(`OpenClaw:MCP 未注册(${cfg})→ 重跑安装命令,或手动执行 `
|
|
555
|
+
+ `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`);
|
|
556
|
+
} else {
|
|
557
|
+
ok(`OpenClaw:MCP 已注册(${entry.url || '?'})`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
457
560
|
return allOk;
|
|
458
561
|
}
|
|
459
562
|
|
|
460
563
|
function uninstall() {
|
|
461
|
-
for (const rel of ['.claude/CLAUDE.md', '.codex/AGENTS.md', '.hermes/SOUL.md'
|
|
564
|
+
for (const rel of ['.claude/CLAUDE.md', '.codex/AGENTS.md', '.hermes/SOUL.md',
|
|
565
|
+
'.openclaw/workspace/AGENTS.md']) {
|
|
462
566
|
if (stripBlock(join(HOME, rel))) ok(`移除规则块 → ${rel}`);
|
|
463
567
|
}
|
|
464
568
|
const cfg = join(HOME, '.claude.json');
|
|
@@ -504,6 +608,16 @@ function uninstall() {
|
|
|
504
608
|
ok(`删除状态目录 → ${stateDir()}`);
|
|
505
609
|
}
|
|
506
610
|
need('Hermes 的 MCP 条目由它自己管 → 需要时执行 hermes mcp remove misakanet');
|
|
611
|
+
|
|
612
|
+
// OpenClaw's MCP entry is one we wrote ourselves, so we can take it back ourselves.
|
|
613
|
+
const ocCfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
614
|
+
const ocData = readJson(ocCfg, null);
|
|
615
|
+
if (ocData?.mcp?.servers?.misakanet) {
|
|
616
|
+
delete ocData.mcp.servers.misakanet;
|
|
617
|
+
backup(ocCfg);
|
|
618
|
+
writeText(ocCfg, `${JSON.stringify(ocData, null, 2)}\n`);
|
|
619
|
+
ok(`移除 MCP 注册 → ${ocCfg}`);
|
|
620
|
+
}
|
|
507
621
|
}
|
|
508
622
|
|
|
509
623
|
// ── main ─────────────────────────────────────────────────────────────
|
|
@@ -548,6 +662,7 @@ if (!targets.length) {
|
|
|
548
662
|
if (agent === 'claude') await installClaude(hookPath, bearer);
|
|
549
663
|
else if (agent === 'codex') await installCodex(hookPath, bearer);
|
|
550
664
|
else if (agent === 'hermes') await installHermes(hookPath);
|
|
665
|
+
else if (agent === 'openclaw') await installOpenclaw(bearer);
|
|
551
666
|
}
|
|
552
667
|
}
|
|
553
668
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@misaka-net/misakanet-setup",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "One-command setup: teach your Claude Code / Codex to search MisakaNet failure lessons before repeating a mistake, and to distil the session at a checkpoint.",
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
-
"misakanet-setup": "
|
|
6
|
+
"misakanet-setup": "bin/misakanet-setup.mjs"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"files": [
|