@misaka-net/misakanet-setup 0.5.0 → 0.5.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 CHANGED
@@ -34,6 +34,9 @@ npx @misaka-net/misakanet-setup --no-register # read-only, no anonymous token
34
34
  npx @misaka-net/misakanet-setup --upgrade # same as installing the latest (the command is idempotent)
35
35
  # re-running also *refreshes* an older hook (the previous copy
36
36
  # is kept as ~/.misakanet-agent/hook.mjs.misakanet.bak)
37
+ npx @misaka-net/misakanet-setup --report # this machine's state as YAML, safe to paste in public
38
+ # (token value never printed, home paths written as ~;
39
+ # two blank fields are yours to fill — see the bounty)
37
40
  npx @misaka-net/misakanet-setup --voice # opt-in: a cue when a search hits, another when it misses
38
41
  # (Claude Code: a PostToolUse hook; mute later with MISAKANET_VOICE=0)
39
42
  npx @misaka-net/misakanet-setup --uninstall # remove exactly what it added
@@ -103,7 +103,7 @@ function readText(path) {
103
103
  * meant to prevent is now caught in CI instead, by a test that binds this literal to the
104
104
  * manifest — a failing test is a better place for that than a request header.
105
105
  */
106
- const VERSION = '0.5.0';
106
+ const VERSION = '0.5.1';
107
107
 
108
108
  function backup(path) {
109
109
  if (DRY || !readText(path)) return;
@@ -466,7 +466,17 @@ async function installClaude(hookPath, bearer) {
466
466
  need('语音钩子:找到了播放器但没有找到音频文件 → 重装一次 npx 包即可(其它功能不受影响)');
467
467
  }
468
468
  }
469
- const post = settings.hooks.PostToolUse || [];
469
+ let post = settings.hooks.PostToolUse || [];
470
+ // Upgrade path: 0.4.2 shipped this entry *without* a matcher, which never fires. Treat a
471
+ // matcher-less entry of ours as stale rather than "already installed", or re-running the
472
+ // installer would leave a hook that stays silent forever (the exact failure this release
473
+ // is about).
474
+ const stale = post.filter((e) => JSON.stringify(e).includes('voice-hook') && e.matcher !== '*');
475
+ if (stale.length) {
476
+ post = post.filter((e) => !stale.includes(e));
477
+ settings.hooks.PostToolUse = post;
478
+ changed = true;
479
+ }
470
480
  if (!JSON.stringify(post).includes('voice-hook')) {
471
481
  // `matcher: '*'` is load-bearing, not cosmetic: measured on 2026-09-16, a PostToolUse
472
482
  // entry **without** a matcher never fired in this host, while the same command with
@@ -865,6 +875,9 @@ async function installOpenclaw(bearer) {
865
875
  ok(`OpenClaw:注册 MCP(streamable-http)→ ${cfg}`);
866
876
  }
867
877
 
878
+ // The last endpoint probe, recorded so `--report` can print it without probing twice.
879
+ let lastProbe = { reachable: false, tools: 0 };
880
+
868
881
  async function verify() {
869
882
  let allOk = true;
870
883
  // Handshake, not a search. `tools/list` proves the endpoint speaks MCP and answers, and it
@@ -875,6 +888,7 @@ async function verify() {
875
888
  // network was broken (found 2026-09-15 by running the agent chain test on this machine).
876
889
  const probe = await mcpRequest('tools/list', {}, 6000, probeEndpoint());
877
890
  const tools = Array.isArray(probe?.tools) ? probe.tools : [];
891
+ lastProbe = { reachable: tools.length > 0, tools: tools.length };
878
892
  if (tools.length) {
879
893
  ok(`端点可达:${ENDPOINT}(MCP 握手成功,${tools.length} 个工具)`);
880
894
  } else if (probe && (probe.error || probe.protocolVersion || probe.serverInfo)) {
@@ -1113,6 +1127,88 @@ function uninstall() {
1113
1127
  }
1114
1128
  }
1115
1129
 
1130
+ /**
1131
+ * `--report`: this machine's state as YAML, safe to paste into a public issue.
1132
+ *
1133
+ * Why it exists: the bounty that asks strangers' machines to run the installer also asks them to
1134
+ * report back — and a report that a human has to assemble by hand is (a) rarely sent and (b)
1135
+ * usually containing their token. So the tool prints its own evidence, redacted by construction,
1136
+ * and leaves only the two things it cannot know (which tools the *agent* sees, and a line of
1137
+ * live-call evidence) as empty fields for the reporter to fill.
1138
+ *
1139
+ * Invariants: the token value never appears (only present/absent); every home path is written as
1140
+ * `~`; no hostname, no IP, no user name. `redact()` is deliberately blunt and applied to every
1141
+ * string, including the messages assembled by the checks.
1142
+ */
1143
+ function redact(value) {
1144
+ return String(value)
1145
+ .split(HOME).join('~')
1146
+ .replace(/mcp_[A-Za-z0-9_-]{8,}/g, '<REDACTED>')
1147
+ .replace(/sk-[A-Za-z0-9_-]{8,}/g, '<REDACTED>')
1148
+ .replace(/Bearer [A-Za-z0-9_.-]{8,}/gi, 'Bearer <REDACTED>');
1149
+ }
1150
+
1151
+ function voiceStatus() {
1152
+ try {
1153
+ const settings = readJson(join(HOME, '.claude', 'settings.json'), null);
1154
+ const buckets = settings?.hooks?.PostToolUse;
1155
+ if (!Array.isArray(buckets)) return 'absent';
1156
+ const ours = buckets.filter((entry) => JSON.stringify(entry).includes('voice-hook'));
1157
+ if (!ours.length) return 'absent';
1158
+ return ours.some((entry) => entry.matcher === '*') ? 'on' : 'stale';
1159
+ } catch {
1160
+ return 'unknown';
1161
+ }
1162
+ }
1163
+
1164
+ function platformName() {
1165
+ if (process.platform === 'win32') return 'windows';
1166
+ if (process.platform === 'darwin') return 'macos';
1167
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return 'wsl2';
1168
+ return process.platform; // linux
1169
+ }
1170
+
1171
+ function distroName() {
1172
+ try {
1173
+ const release = readFileSync('/etc/os-release', 'utf8');
1174
+ const id = release.match(/^ID="?([a-z0-9._-]+)"?$/m);
1175
+ return id ? id[1] : 'unknown';
1176
+ } catch {
1177
+ return 'n/a';
1178
+ }
1179
+ }
1180
+
1181
+ async function report() {
1182
+ const allOk = await verify();
1183
+ const agents = AGENTS.filter((agent) => detect(agent));
1184
+ const token = existsSync(join(stateDir(), 'token'));
1185
+ const lines = [
1186
+ '# MisakaNet setup report — safe to paste in public; the two empty fields at the end are',
1187
+ '# for you to fill (they are the only parts this tool cannot know).',
1188
+ 'schema: misakanet-setup-report/1',
1189
+ `setup-version: ${VERSION}`,
1190
+ `os: ${platformName()}`,
1191
+ `distro: ${distroName()}`,
1192
+ `arch: ${process.arch}`,
1193
+ `node: ${process.version}`,
1194
+ `detected-agents: [${agents.join(', ')}]`,
1195
+ `verify: ${allOk ? 'READY' : 'NOT READY'}`,
1196
+ `endpoint-reachable: ${lastProbe.reachable}`,
1197
+ `endpoint-tools: ${lastProbe.tools}`,
1198
+ `token: ${token ? 'present' : 'absent'}`,
1199
+ `hook: ${existsSync(join(stateDir(), 'hook.mjs')) ? 'present' : 'absent'}`,
1200
+ `voice: ${voiceStatus()}`,
1201
+ `open-items: ${manual.length}`,
1202
+ ...(manual.length
1203
+ ? ['open-items-detail:', ...manual.map((m) => ` - ${redact(m)}`)]
1204
+ : []),
1205
+ '# Fill these two in (see the bounty for how):',
1206
+ 'tools-visible: {} # e.g. {codex: 7} — from `codex mcp list` / `codewhale mcp tools`',
1207
+ 'live-call-evidence: "" # one line your agent printed when it called misakanet_search',
1208
+ ];
1209
+ console.log(lines.join('\n'));
1210
+ }
1211
+
1116
1212
  // ── main ─────────────────────────────────────────────────────────────
1117
1213
  function render() {
1118
1214
  const out = [];
@@ -1122,9 +1218,15 @@ function render() {
1122
1218
  return out.join('\n');
1123
1219
  }
1124
1220
 
1125
- const mode = has('--uninstall') ? 'uninstall' : (has('--verify') ? 'verify' : 'install');
1126
- console.log(`MisakaNet 安装程序(npx 版)${DRY ? '(--dry-run,不会写任何文件)' : ''}${has('--upgrade') ? '(--upgrade:与安装等价,覆盖安装即升级)' : ''}`);
1127
- console.log(`家目录:${HOME}\n`);
1221
+ const mode = has('--uninstall') ? 'uninstall'
1222
+ : (has('--report') ? 'report'
1223
+ : (has('--verify') ? 'verify' : 'install'));
1224
+ if (mode !== 'report') {
1225
+ // The banner names the home directory, which is exactly the kind of thing that gets pasted
1226
+ // along with a report — so `--report` prints only the report.
1227
+ console.log(`MisakaNet 安装程序(npx 版)${DRY ? '(--dry-run,不会写任何文件)' : ''}${has('--upgrade') ? '(--upgrade:与安装等价,覆盖安装即升级)' : ''}`);
1228
+ console.log(`家目录:${HOME}\n`);
1229
+ }
1128
1230
 
1129
1231
  if (mode === 'uninstall') {
1130
1232
  uninstall();
@@ -1133,6 +1235,14 @@ if (mode === 'uninstall') {
1133
1235
  process.exit(0);
1134
1236
  }
1135
1237
 
1238
+ if (mode === 'report') {
1239
+ // Reporting must work before an install too: a bot's first useful data point is often "this
1240
+ // machine has no agent config at all". Exit 0 even when NOT READY — the report is evidence,
1241
+ // not a gate.
1242
+ await report();
1243
+ process.exit(0);
1244
+ }
1245
+
1136
1246
  if (mode === 'verify') {
1137
1247
  const allOk = await verify();
1138
1248
  console.log(render());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misaka-net/misakanet-setup",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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"