@misaka-net/misakanet-setup 0.8.0 → 0.9.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,9 +1,14 @@
1
1
  # @misaka-net/misakanet-setup
2
2
 
3
3
  One command to teach your **Claude Code**, **Codex**, **Hermes**, **OpenClaw**, **codewhale**,
4
- **Cursor**, **Gemini CLI**, **Copilot CLI**, **OpenCode**, **Kiro** or **MiniMax Code** to
5
- check MisakaNet's failure lessons before repeating a mistake — and to write up the reusable lessons
6
- this session produced, at a checkpoint.
4
+ **Cursor**, **Gemini CLI**, **Copilot CLI**, **OpenCode**, **Kiro**, **MiniMax Code** or
5
+ **DeepSeek Harness** to check MisakaNet's failure lessons before repeating a mistake — and to write
6
+ up the reusable lessons this session produced, at a checkpoint.
7
+
8
+ For **DeepSeek Harness (dsh)** that command writes the **rules half only** (`$DSH_HOME/AGENTS.md`)
9
+ and prints the one command its tools need (`dsh plugin --profile web add misakanet`); everything
10
+ else in this README applies to all twelve exactly as written. The split, and why dsh is the one
11
+ target shaped that way, is in [dsh: the rules half, written](#dsh-the-rules-half-written--the-plugin-half-printed).
7
12
 
8
13
  **What it writes:** an MCP endpoint into each agent config, rules blocks for the clients that take
9
14
  one, and a local hook for Claude Code. **What leaves this machine:** at most one anonymous request
@@ -47,60 +52,66 @@ look installed and register nothing. Its manifest also comes with the rules bloc
47
52
  (`NODE_USE_ENV_PROXY=1`) are in `docs/integrations/mcode.md`.
48
53
 
49
54
  All of them 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 |
50
- | 2 | the agent **knows when** | rules block appended to `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md` / `~/.hermes/SOUL.md` / `~/.openclaw/workspace/AGENTS.md` / `~/.minimax/AGENTS.md` (mcode's global scope). **The five JSON-config clients are the exception: they get none**, because their rules are project-scoped (`.cursor/rules/*.mdc`, `GEMINI.md`, `.github/copilot-instructions.md`, `AGENTS.md`, `.kiro/steering/*.md`) and the installer cannot know where your projects live — the install output says so, with the project-side action for each client, instead of implying behaviour changed (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 |
55
+ | 2 | the agent **knows when** | rules block appended to `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md` / `~/.hermes/SOUL.md` / `~/.openclaw/workspace/AGENTS.md` / `~/.minimax/AGENTS.md` (mcode's global scope) / `$DSH_HOME/AGENTS.md` (dsh, default `~/.dsh`). **The five JSON-config clients are the exception: they get none**, because their rules are project-scoped (`.cursor/rules/*.mdc`, `GEMINI.md`, `.github/copilot-instructions.md`, `AGENTS.md`, `.kiro/steering/*.md`) and the installer cannot know where your projects live — the install output says so, with the project-side action for each client, instead of implying behaviour changed (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 |
51
56
  | 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` |
52
57
  | 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 "write up the reusable lessons and submit them" reminder — the agent does that writing and that submitting, by hand, and the hook itself sends nothing by default; a failed tool call injects a "search before you retry" reminder built from the error text, and only with `MISAKANET_HOOK_FETCH=1` (off by default) does the hook itself send that error text as a search query. 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 |
53
58
 
54
59
  Without (3) a rule saying "summarise every 20 turns" never fires — agents do not keep
55
60
  counters. Without (1)/(2) the hook has nothing to call.
56
61
 
57
- ### dsh is not a file target — it is a plugin
62
+ ### dsh: the rules half, written — the plugin half, printed
58
63
 
59
64
  dsh takes its MCP servers from a **plugin bundle**, not from a JSON file. Its instruction files are a
60
65
  chain: **`$DSH_HOME/AGENTS.md`** (user-global, default `~/.dsh/AGENTS.md`, applies to every session)
61
66
  first, then the project's `AGENTS.md`/`CLAUDE.md` from the project root down to the working directory
62
67
  (`@deepseek-ai/dsh-agent-instructions`, byte-budgeted at 65,536 by default). It has no Claude-Code-style
63
- hooks.
68
+ hooks — and no plugin extension point that could inject a rule, which is why the rules are a **file**
69
+ and the tools are a **plugin**, and why the two cannot be one write.
64
70
 
65
- So dsh takes **two manual steps that do not know about each other**, and this installer does neither of
66
- them — it writes no file under `~/.dsh`:
71
+ So dsh is **one file target and one printed step**. The installer writes the half a file can hold and
72
+ names the other half instead of doing neither:
67
73
 
68
74
  ```bash
69
- dsh plugin --profile web add misakanet # ① the tools (mcp__misakanet__*) — dsh's own command
70
- # ② you paste the rules block into ~/.dsh/AGENTS.md (global) or the project's AGENTS.md
75
+ npx @misaka-net/misakanet-setup --only dsh # ① the rules block → $DSH_HOME/AGENTS.md
76
+ dsh plugin --profile web add misakanet # ② the tools (mcp__misakanet__*) — dsh's own command
71
77
  ```
72
78
 
79
+ Order does not matter; skipping ② leaves rules without tools, and the run says so with the exact
80
+ command (`! dsh:工具在插件包里,安装器不写这个文件 → 跑 \`dsh plugin --profile web add misakanet\``).
81
+ `--list-writes --only dsh` names the file and its tier (`tier2 ② 写规则块 … ~/.dsh/AGENTS.md`), and
82
+ `--mcp-only` skips it, like every other rules block.
83
+
73
84
  Measured 2026-10-17: writing `~/.dsh/AGENTS.md` is picked up as `user-global` instructions on the next
74
- message, with no restart.
85
+ message, with no restart. Measured 2026-10-17: installing the plugin takes ~1s into
86
+ `~/.dsh/profiles/web`, and neither `index.js` (the MCP client mount) nor `lib/client.js` (the web
87
+ panel) injects a rule.
75
88
 
76
- Measured 2026-10-17: installing the current `misakanet` takes ~1s into `~/.dsh/profiles/web`; the plugin's
77
- `index.js` mounts the MCP client and `lib/client.js` is the web panel — **neither injects a rule**,
78
- which is why ② is a separate, manual step.
89
+ Measured 2026-10-30, on a machine whose block had been pasted by hand: the unmarked text was adopted
90
+ into a marked block (`<!-- misakanet:start -->` … `<!-- misakanet:end -->`, header preserved,
91
+ 1,474 bytes) and the next message reloaded it — so an older hand-pasted copy is upgraded in place
92
+ rather than duplicated.
79
93
 
80
- **Upgrading the installer does not refresh that block.** Nothing rewrites text you pasted, and the
81
- installer only refreshes the files it manages (for Claude Code, `~/.claude/CLAUDE.md`). Measured
82
- 2026-10-23 (issue #3773): one machine's `~/.dsh/AGENTS.md` still carried the old block — its last line
83
- read `脱敏:密钥/凭据→<REDACTED>…` — while the same installer had just written the current one
84
- (`脱敏:已由代码自动完成…`) to `~/.claude/CLAUDE.md`. Re-paste after an upgrade, and check by comparing
85
- that line; the copy in `docs/integrations/dsh.md` can lag too.
94
+ **A sandbox home stays a sandbox.** `DSH_HOME` is a user setting, so it is honoured **only when the
95
+ installer is addressing the real home**; with `--home <dir>` every write stays under `<dir>`. That is
96
+ not a footnote: before 2026-10-30 the package's own test run wrote the *real* `~/.dsh/AGENTS.md`
97
+ (truncated it to 35 bytes) because the suite inherits `DSH_HOME` from the shell.
86
98
 
87
- **Uninstalling takes two steps, and the second one is yours.** `dsh plugin --profile web remove misakanet`
88
- removes the tools; the rules block stays in `~/.dsh/AGENTS.md` (or the project's `AGENTS.md`), and there is
89
- no `misakanet:start` / `misakanet:end` marker to lift it — the installer never put one there, because it
90
- never wrote that file. Remove the block by hand, then check:
99
+ **Uninstalling is two steps, and neither takes the other's half:**
91
100
 
92
101
  ```bash
93
- grep -c misakanet "${DSH_HOME:-$HOME/.dsh}/AGENTS.md" # expected 0; a missing file errors, which is also clean
102
+ npx @misaka-net/misakanet-setup --uninstall # ① strips the marked block
103
+ dsh plugin --profile web remove misakanet # ② removes the tools
94
104
  ```
95
105
 
96
- Until the block is removed, a new dsh session still follows it and calls `mcp__misakanet__*` tools that
97
- no longer exist.
106
+ Doing ① alone leaves a plugin nobody's rules mention; doing ② alone leaves a session following rules
107
+ that call tools which no longer exist. `--uninstall` keeps `AGENTS.md.misakanet.bak` next to the file
108
+ it emptied, and deletes the file outright when it held nothing but the block.
98
109
 
99
- **Why it is not mechanised yet.** dsh's MCP servers come from the plugin bundle and its instructions come
100
- from the `AGENTS.md` chain — two entry points this installer has no path into: it only writes the files on
101
- its own target list, and dsh's files are not on it. A mechanical fix (a marker pair around the block, plus
102
- an uninstall branch) **is not implemented; it is follow-up work**. Until then,
103
- `dsh plugin --profile web remove misakanet` does not take the block with it.
110
+ **Upgrades now refresh the block**, which is the one thing the old hand-pasted flow could not do
111
+ (measured 2026-10-23, issue #3773: a machine's block was still the old `脱敏:密钥/凭据→<REDACTED>…`
112
+ wording while the same installer had just written the current one to `~/.claude/CLAUDE.md`). Re-running
113
+ the installer updates it in place; the copy in `docs/integrations/dsh.md` can still lag, and the
114
+ installer's output is the criterion.
104
115
 
105
116
  Full guide: `docs/integrations/dsh.md`.
106
117
 
@@ -132,7 +132,7 @@ const SILENT = has('--silent');
132
132
  const REPORT_JSON = has('--report-json');
133
133
  const HOME = resolve(valueOf('--home', homedir()));
134
134
  const AGENTS = ['claude', 'codex', 'hermes', 'openclaw', 'codewhale',
135
- 'cursor', 'gemini', 'copilot', 'opencode', 'kiro', 'mcode'];
135
+ 'cursor', 'gemini', 'copilot', 'opencode', 'kiro', 'mcode', 'dsh'];
136
136
 
137
137
  /**
138
138
  * The three line lists, and what every line knows about itself.
@@ -197,7 +197,7 @@ function readText(path) {
197
197
  * except the two that name the version (measured 2026-10-22: the 0.6.0 release pull request
198
198
  * failed 9 e2e legs on exactly that).
199
199
  */
200
- const VERSION = '0.8.0' // x-release-please-version
200
+ const VERSION = '0.9.0' // x-release-please-version
201
201
 
202
202
  /**
203
203
  * The shape of a client id, in one place.
@@ -483,6 +483,8 @@ const AGENT_LABELS = {
483
483
  hermes: 'Hermes',
484
484
  openclaw: 'OpenClaw',
485
485
  codewhale: 'Codewhale',
486
+ // dsh is a plugin-bundle client: the installer writes only its rules half (see installDsh).
487
+ dsh: 'dsh',
486
488
  ...Object.fromEntries(Object.entries(MCP_ONLY_TARGETS).map(([id, spec]) => [id, spec.label])),
487
489
  // Not an MCP-only target: it takes a rules block too (see `installMcode`), so its label cannot be
488
490
  // read from that table.
@@ -566,6 +568,9 @@ const AGENT_WRITE_PATHS = {
566
568
  // carries the rules block (②). Listing both here is what makes the preview, `--list-writes` and
567
569
  // the permission pre-check agree with `installMcode`.
568
570
  mcode: (home) => [join(home, MCODE_PLUGIN_REL), join(home, MCODE_RULES_REL)],
571
+ // dsh reads its instructions from $DSH_HOME/AGENTS.md (default ~/.dsh). Its MCP servers
572
+ // come from a plugin bundle this installer cannot write, so one file is the whole manifest.
573
+ dsh: (home) => [join(dshHome(home), 'AGENTS.md')],
569
574
  openclaw: () => openclawWorkspaces().map((workspace) => join(workspace, 'AGENTS.md')),
570
575
  };
571
576
 
@@ -1245,6 +1250,40 @@ let lastResponseOk = false;
1245
1250
  // `--verify` can print is what actually came back (`{}` is not a diagnosis).
1246
1251
  let lastResponsePreview = '';
1247
1252
 
1253
+ // The server's retry horizon on the last response, in seconds, when it sent one: the `Retry-After`
1254
+ // response header (seconds or an HTTP-date), the standard shape `docs/index.html` already reads on
1255
+ // 429s. Per-request, so a horizon from an earlier call can never leak into a later failure's words.
1256
+ let lastRetryAfterSeconds = null;
1257
+
1258
+ /** `Retry-After` → seconds. Both forms the header allows; anything else is "no horizon", not 0. */
1259
+ function retryAfterSeconds(headerValue) {
1260
+ const raw = String(headerValue || '').trim();
1261
+ if (!raw) return null;
1262
+ if (/^\d+$/.test(raw)) return Number(raw);
1263
+ const at = Date.parse(raw);
1264
+ if (Number.isNaN(at)) return null;
1265
+ return Math.max(0, Math.round((at - Date.now()) / 1000));
1266
+ }
1267
+
1268
+ /**
1269
+ * How long the server says to wait before re-running, when it says. The failure body's `retry_after`
1270
+ * (seconds, HTTP `Retry-After` semantics) wins over the header, and neither is guessed: a server that
1271
+ * stays silent gets no horizon sentence at all.
1272
+ */
1273
+ function retryHorizonOf(result) {
1274
+ const fromBody = result && (result.retry_after ?? result.retry_after_seconds);
1275
+ const n = Number(fromBody);
1276
+ if (Number.isFinite(n) && n > 0) return Math.round(n);
1277
+ return lastRetryAfterSeconds;
1278
+ }
1279
+
1280
+ /** "45 秒" / "15 分钟" / "2 小时" — the horizon in the words a person reads a clock in. */
1281
+ function humanWait(seconds) {
1282
+ if (seconds < 60) return `${seconds} 秒`;
1283
+ if (seconds < 3600) return `${Math.round(seconds / 60)} 分钟`;
1284
+ return `${Math.round(seconds / 3600)} 小时`;
1285
+ }
1286
+
1248
1287
  async function mcpRequest(method, params, timeoutMs = 6000, urlOverride = '') {
1249
1288
  const headers = {
1250
1289
  'Content-Type': 'application/json',
@@ -1260,6 +1299,7 @@ async function mcpRequest(method, params, timeoutMs = 6000, urlOverride = '') {
1260
1299
  lastRequestKind = '';
1261
1300
  lastResponseOk = false;
1262
1301
  lastResponsePreview = '';
1302
+ lastRetryAfterSeconds = null;
1263
1303
  try {
1264
1304
  const response = await fetch(urlOverride || ENDPOINT, {
1265
1305
  method: 'POST',
@@ -1271,6 +1311,9 @@ async function mcpRequest(method, params, timeoutMs = 6000, urlOverride = '') {
1271
1311
  // below, not by this line — but "it answered with something that is not a handshake" and
1272
1312
  // "nothing answered" are different facts (see probeOutcome()).
1273
1313
  lastResponseOk = true;
1314
+ lastRetryAfterSeconds = retryAfterSeconds(
1315
+ response.headers && typeof response.headers.get === 'function'
1316
+ ? response.headers.get('Retry-After') : null);
1274
1317
  const payload = await response.json();
1275
1318
  lastResponsePreview = JSON.stringify(payload).slice(0, 160);
1276
1319
  if (payload && payload.error) {
@@ -1618,8 +1661,15 @@ async function ensureIdentity() {
1618
1661
  : serverCode
1619
1662
  ? `服务端拒绝了这次注册(${serverCode})`
1620
1663
  : `多半是网络(${lastRequestError || '没有收到响应'})`;
1664
+ // …and when the server says *how long* to wait, say that too instead of "稍后". The horizon is
1665
+ // never guessed and never rounded into a promise: a server that sent none keeps the vague
1666
+ // wording, because a made-up wait is the same lie as the network guess this replaced (issue
1667
+ // #3511 — the retry-horizon half; the `code` half landed with #3512).
1668
+ const horizon = retryHorizonOf(result);
1621
1669
  need(`匿名注册没成功:${cause} → 读课程不受影响,但写入类工具(write_lesson)现在用不了;`
1622
- + '稍后重跑本命令就能拿到 token'
1670
+ + (horizon
1671
+ ? `约 ${humanWait(horizon)}后重跑本命令就能拿到 token(服务端给出的重试窗口)`
1672
+ : '稍后重跑本命令就能拿到 token')
1623
1673
  + (serverHint ? `(服务端提示:${serverHint})` : ''));
1624
1674
  return '';
1625
1675
  }
@@ -1659,6 +1709,9 @@ function mcpEntryPresent(agent) {
1659
1709
  }
1660
1710
 
1661
1711
  function detect(agent) {
1712
+ // dsh may live outside $HOME (DSH_HOME), so it is answered by its own resolver rather
1713
+ // than by the relative-path table below.
1714
+ if (agent === 'dsh') return existsSync(dshHome());
1662
1715
  const paths = {
1663
1716
  claude: ['.claude.json', '.claude'],
1664
1717
  codex: ['.codex'],
@@ -2287,6 +2340,43 @@ async function installMcpOnly(agent, bearer) {
2287
2340
  * `adoptExistingClientId`, so a re-run without `--client-id` keeps the header the file already
2288
2341
  * declares (#3772).
2289
2342
  */
2343
+ /**
2344
+ * Where dsh keeps its home: `DSH_HOME` wins, `<home>/.dsh` otherwise (#100.4 item 4).
2345
+ *
2346
+ * `DSH_HOME` is a *user* setting, so it may only apply when the install is addressing the real
2347
+ * home. `--home <dir>` means "pretend this directory is the home" — the test suite and the e2e
2348
+ * depend on every write staying inside it. Honouring `DSH_HOME` there wrote the user's real
2349
+ * `~/.dsh/AGENTS.md` from inside a sandbox run (measured 2026-10-30: the suite truncated it to
2350
+ * 35 bytes, and the next message reloaded the broken block).
2351
+ */
2352
+ function dshHome(home = HOME) {
2353
+ const real = homedir();
2354
+ if (process.env.DSH_HOME && resolve(home) === resolve(real)) return process.env.DSH_HOME;
2355
+ return join(home, '.dsh');
2356
+ }
2357
+
2358
+ /**
2359
+ * dsh is a plugin-bundle client, so only half of it is a file target.
2360
+ *
2361
+ * The tools (`mcp__misakanet__*`) come from the `misakanet` plugin bundle, which dsh installs with
2362
+ * its own command — there is no JSON file this installer could write. The rules, however, live in
2363
+ * `$DSH_HOME/AGENTS.md`, and writing them is exactly the manual step the README used to ask for.
2364
+ * So the installer writes the half it can and prints the half it cannot, instead of doing neither
2365
+ * and leaving the user with two steps that do not know about each other.
2366
+ */
2367
+ async function installDsh() {
2368
+ const rules = join(dshHome(), 'AGENTS.md');
2369
+ const profile = process.env.DSH_PROFILE || 'web';
2370
+ const manual = `dsh plugin --profile ${profile} add misakanet`;
2371
+ if (!MCP_ONLY) {
2372
+ ok(`dsh:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
2373
+ } else {
2374
+ skip('dsh:规则块未写(--mcp-only)');
2375
+ }
2376
+ need(`dsh:工具在插件包里,安装器不写这个文件 → 跑 \`${manual}\``
2377
+ + '(不跑就只有规则、没有 mcp__misakanet__* 工具)');
2378
+ }
2379
+
2290
2380
  async function installMcode(bearer) {
2291
2381
  const plugin = mcodePluginPath(HOME);
2292
2382
  const existing = readText(plugin);
@@ -2781,6 +2871,23 @@ async function verify() {
2781
2871
  }
2782
2872
  }
2783
2873
  }
2874
+ // dsh: one file, and only half of the client — so the report says which half it can see. The
2875
+ // rules block is our footprint and is checked like every other target's; the tools live in the
2876
+ // plugin's profile, which this process cannot read, and a "complete" that quietly skipped that
2877
+ // would be the false green this whole report exists to avoid (measured 2026-10-30: with the
2878
+ // block deleted, `--verify` still said 安装完整性:完整).
2879
+ if (wantClient('dsh') && detect('dsh')) {
2880
+ const rules = join(dshHome(), 'AGENTS.md');
2881
+ if (!readText(rules).includes(`<!-- ${START} -->`)) {
2882
+ needWrite('dsh', rules,
2883
+ `dsh:规则块没装(${rules})→ 重跑安装命令`,
2884
+ `dsh:规则块装不了(${rules})→ ${unwritableAdvice()}`);
2885
+ } else {
2886
+ ok('dsh:规则块已装', 'dsh');
2887
+ skip(`dsh:工具在插件包里,--verify 看不到它 → 跑 \`dsh plugin --profile ${process.env.DSH_PROFILE || 'web'} list\` 复核`,
2888
+ 'dsh');
2889
+ }
2890
+ }
2784
2891
  // Version: what is installed, and whether the registry has moved on. Read-only here — the
2785
2892
  // hook owns the periodic nudge (it cannot be a network call in this process's hot path), and
2786
2893
  // `--verify` is an explicit user action, so one lookup is both cheap and expected.
@@ -3130,6 +3237,7 @@ function mcodePluginDirectoryOwnership() {
3130
3237
 
3131
3238
  function uninstall() {
3132
3239
  let hermesRulesRemoved = false;
3240
+ if (stripBlock(join(dshHome(), 'AGENTS.md'))) ok(`移除规则块 → ${join(dshHome(), 'AGENTS.md')}`);
3133
3241
  for (const rel of ['.claude/CLAUDE.md', '.codex/AGENTS.md', '.hermes/SOUL.md', '.minimax/AGENTS.md']) {
3134
3242
  if (stripBlock(join(HOME, rel))) {
3135
3243
  ok(`移除规则块 → ${rel}`);
@@ -3771,6 +3879,7 @@ try {
3771
3879
  else if (agent === 'openclaw') await installOpenclaw(bearer);
3772
3880
  else if (agent === 'codewhale') await installCodewhale(bearer);
3773
3881
  else if (agent === 'mcode') await installMcode(bearer);
3882
+ else if (agent === 'dsh') await installDsh();
3774
3883
  else if (MCP_ONLY_TARGETS[agent]) await installMcpOnly(agent, bearer);
3775
3884
  installed += 1;
3776
3885
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misaka-net/misakanet-setup",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "MisakaNet failure lessons for your agents: writes an MCP endpoint into each agent config, rules blocks where the client takes one, and a Claude Code hook. At most one anonymous request leaves this machine at install; then only the agent's calls — unless MISAKANET_HOOK_FETCH=1 (off by default) the hook sends the failed call's error text or command.",
5
5
  "bin": {
6
6
  "misakanet-setup": "bin/misakanet-setup.mjs"