@misaka-net/misakanet-setup 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # @misaka-net/misakanet-setup
2
2
 
3
- One command to teach your **Claude Code** or **Codex** to check MisakaNet's failure lessons
4
- before repeating a mistake — and to distil the session's reusable lessons at a checkpoint.
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` / `~/.codex/config.toml`, with the Bearer token so reads are not metered by the anonymous 5/day/IP limit |
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`, with the Bearer token so reads are not metered by the anonymous 5/day/IP limit. Hermes and OpenClaw own their MCP registries, so theirs is registered through their own CLIs (`hermes mcp add …`, `openclaw mcp add …`) instead of by editing a file we do not own |
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.
@@ -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 (detects Claude Code / Codex / Hermes)
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
@@ -23,7 +23,8 @@
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 } from 'node:fs';
26
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync, chmodSync, accessSync, constants } from 'node:fs';
27
+ import { spawnSync } from 'node:child_process';
27
28
  import { homedir } from 'node:os';
28
29
  import { join, dirname, resolve } from 'node:path';
29
30
  import { fileURLToPath } from 'node:url';
@@ -52,7 +53,7 @@ const valueOf = (flag, dflt) => {
52
53
  const only = valueOf('--only', '').split(',').map((s) => s.trim()).filter(Boolean);
53
54
  const DRY = has('--dry-run');
54
55
  const HOME = resolve(valueOf('--home', homedir()));
55
- const AGENTS = ['claude', 'codex', 'hermes'];
56
+ const AGENTS = ['claude', 'codex', 'hermes', 'openclaw'];
56
57
 
57
58
  const done = [];
58
59
  const manual = [];
@@ -77,6 +78,16 @@ function readText(path) {
77
78
  }
78
79
  }
79
80
 
81
+ /**
82
+ * Own version, read from the manifest rather than typed twice.
83
+ *
84
+ * It rode along as a literal `misakanet-setup/0.1.0` while the package moved on - the same
85
+ * hardcoded-version drift this repository has had to fix on the site badge and in JOIN.md.
86
+ * The tarball always carries package.json (npm cannot publish without it), so the fallback
87
+ * only covers a hand-copied file.
88
+ */
89
+ const PKG_VERSION = readJson(join(PKG_ROOT, 'package.json'), {}).version || '0.0.0';
90
+
80
91
  function backup(path) {
81
92
  if (DRY || !readText(path)) return;
82
93
  try {
@@ -145,7 +156,7 @@ async function mcpCall(tool, toolArgs, bearer = '', timeoutMs = 6000, urlOverrid
145
156
  Accept: 'application/json',
146
157
  'MCP-Protocol-Version': '2025-06-18',
147
158
  Origin: 'https://misakanet.org',
148
- 'User-Agent': 'misakanet-setup/0.1.0',
159
+ 'User-Agent': `misakanet-setup/${PKG_VERSION}`,
149
160
  };
150
161
  if (bearer) headers.Authorization = `Bearer ${bearer}`;
151
162
  const controller = new AbortController();
@@ -268,10 +279,39 @@ async function ensureIdentity() {
268
279
 
269
280
  // ── per-agent install ────────────────────────────────────────────────
270
281
  function detect(agent) {
271
- const paths = { claude: ['.claude.json', '.claude'], codex: ['.codex'], hermes: ['.hermes'] }[agent] || [];
282
+ const paths = {
283
+ claude: ['.claude.json', '.claude'],
284
+ codex: ['.codex'],
285
+ hermes: ['.hermes'],
286
+ openclaw: ['.openclaw'],
287
+ }[agent] || [];
272
288
  return paths.some((p) => existsSync(join(HOME, p)));
273
289
  }
274
290
 
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
+
275
315
  async function installClaude(hookPath, bearer) {
276
316
  const cfg = join(HOME, '.claude.json');
277
317
  const data = readJson(cfg, null);
@@ -399,6 +439,51 @@ async function installHermes(hookPath) {
399
439
  }
400
440
  }
401
441
 
442
+ /**
443
+ * OpenClaw: rules live in ~/.openclaw/workspace, and the agent has its own MCP client
444
+ * (`openclaw mcp add`), so this target registers through that CLI instead of editing a
445
+ * config file we do not own. Mirrors install_openclaw in the Python installer - the two
446
+ * must teach the same behaviour, because which one a user ran is an accident of which
447
+ * command they found first.
448
+ *
449
+ * The CLI keeps its cache under $XDG_CACHE_HOME, which may be read-only in a sandbox; that
450
+ * surfaces as "Could not start the CLI", hence the explicit hint rather than a raw stderr.
451
+ */
452
+ async function installOpenclaw(bearer) {
453
+ const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
454
+ const manual = `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`;
455
+ if (!existsSync(dirname(rules))) {
456
+ need(`OpenClaw:找不到 ${dirname(rules)} → 先运行一次 openclaw 生成 workspace`);
457
+ } else {
458
+ ok(`OpenClaw:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
459
+ }
460
+
461
+ const cli = findOnPath('openclaw');
462
+ if (!cli) {
463
+ need(`OpenClaw:没找到 openclaw CLI → 手动执行 \`${manual}\``);
464
+ return;
465
+ }
466
+ if (DRY) {
467
+ ok(`OpenClaw:会执行 \`${manual} --no-probe\``);
468
+ return;
469
+ }
470
+ const cmd = [cli, 'mcp', 'add', 'misakanet', '--url', ENDPOINT,
471
+ '--transport', 'streamable-http', '--no-probe'];
472
+ if (bearer) cmd.push('--header', `Authorization=Bearer ${bearer}`);
473
+ const result = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8', timeout: 20000 });
474
+ const out = `${result.stdout || ''}${result.stderr || ''}`;
475
+ if (result.status === 0) {
476
+ ok('OpenClaw:已注册 MCP `misakanet`(openclaw mcp add)');
477
+ return;
478
+ }
479
+ if (out.includes('Could not start the CLI')) {
480
+ need('OpenClaw:CLI 起不动(缓存目录不可写)→ 设 XDG_CACHE_HOME 到可写目录后重跑,'
481
+ + `或手动执行:${manual}`);
482
+ } else {
483
+ need(`OpenClaw:\`openclaw mcp add\` 失败:${out.trim().slice(0, 200) || `退出码 ${result.status}`}`);
484
+ }
485
+ }
486
+
402
487
  async function verify() {
403
488
  let allOk = true;
404
489
  const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 }, '', 6000, probeEndpoint());
@@ -454,11 +539,44 @@ async function verify() {
454
539
  if (!entry) { allOk = false; need(`Claude Code:MCP 未注册(${cfg})`); }
455
540
  else ok(`Claude Code:MCP 已注册(${entry.url})`);
456
541
  }
542
+ // OpenClaw is only reported when the user actually has it: telling a machine without
543
+ // OpenClaw that it is "not ready" would be a lie about a target that was never selected.
544
+ // The MCP state is asked of the CLI that owns it rather than guessed from its config.
545
+ if (detect('openclaw')) {
546
+ const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
547
+ if (!readText(rules).includes(`<!-- ${START} -->`)) {
548
+ allOk = false;
549
+ need(`OpenClaw:规则块没装(${rules})→ 重跑安装命令`);
550
+ } else {
551
+ ok('OpenClaw:规则块已装');
552
+ }
553
+ const cli = findOnPath('openclaw');
554
+ if (!cli) {
555
+ allOk = false;
556
+ need(`OpenClaw:没找到 openclaw CLI → 无法确证 MCP 是否注册;手动执行 `
557
+ + `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`);
558
+ } else {
559
+ const listed = spawnSync(cli, ['mcp', 'list'], { encoding: 'utf8', timeout: 15000 });
560
+ const out = `${listed.stdout || ''}${listed.stderr || ''}`;
561
+ if (listed.status === 0 && /misakanet/.test(out)) {
562
+ ok('OpenClaw:MCP 已注册(openclaw mcp list)');
563
+ } else if (listed.status === 0) {
564
+ allOk = false;
565
+ need(`OpenClaw:MCP 未注册 → 重跑安装命令,或手动执行 `
566
+ + `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`);
567
+ } else {
568
+ allOk = false;
569
+ need(`OpenClaw:\`openclaw mcp list\` 没跑通 → 无法确证 MCP 状态`
570
+ + `${out.trim() ? `(${out.trim().slice(0, 120)})` : ''};可先手工确认一次`);
571
+ }
572
+ }
573
+ }
457
574
  return allOk;
458
575
  }
459
576
 
460
577
  function uninstall() {
461
- for (const rel of ['.claude/CLAUDE.md', '.codex/AGENTS.md', '.hermes/SOUL.md']) {
578
+ for (const rel of ['.claude/CLAUDE.md', '.codex/AGENTS.md', '.hermes/SOUL.md',
579
+ '.openclaw/workspace/AGENTS.md']) {
462
580
  if (stripBlock(join(HOME, rel))) ok(`移除规则块 → ${rel}`);
463
581
  }
464
582
  const cfg = join(HOME, '.claude.json');
@@ -504,6 +622,7 @@ function uninstall() {
504
622
  ok(`删除状态目录 → ${stateDir()}`);
505
623
  }
506
624
  need('Hermes 的 MCP 条目由它自己管 → 需要时执行 hermes mcp remove misakanet');
625
+ need('OpenClaw 的 MCP 条目由它自己管 → 需要时执行 openclaw mcp remove misakanet');
507
626
  }
508
627
 
509
628
  // ── main ─────────────────────────────────────────────────────────────
@@ -548,6 +667,7 @@ if (!targets.length) {
548
667
  if (agent === 'claude') await installClaude(hookPath, bearer);
549
668
  else if (agent === 'codex') await installCodex(hookPath, bearer);
550
669
  else if (agent === 'hermes') await installHermes(hookPath);
670
+ else if (agent === 'openclaw') await installOpenclaw(bearer);
551
671
  }
552
672
  }
553
673
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@misaka-net/misakanet-setup",
3
- "version": "0.1.0",
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.0",
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": "./bin/misakanet-setup.mjs"
6
+ "misakanet-setup": "bin/misakanet-setup.mjs"
7
7
  },
8
8
  "type": "module",
9
9
  "files": [