@horizon_works/banto 0.8.0 → 0.8.2

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.
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mcp_config.json へ番頭を1ブロック足す(★既にあるものを消さない)
4
+ *
5
+ * 🚨 由来: 2026-08-28。ターミナル1行で入れられるようにしたら、
6
+ * ★**既存の設定を消した**。原因=ファイルが BOM つき UTF-8 で `JSON.parse` が落ち、
7
+ * ★`catch` が「無い」とみなして空から作り直していた。
8
+ * ★★**読めなかったことと、無いことは別**(→ premise-check §3)。
9
+ *
10
+ * ★この道具の約束
11
+ * ① 読めなければ **書かない**(★控えを作って、人に見てもらう)
12
+ * ② 既にある `mcpServers` は **そのまま残す**
13
+ * ③ 書く前に **控えを取る**(.bak-日時)
14
+ * ④ 書いたあと **読み直して確かめる**(★書けたと言う前に)
15
+ *
16
+ * 使い方:
17
+ * node add_to_config.js <鍵.署名> [設定ファイルのパス]
18
+ * ★パスを省くと、この環境の既定の場所を探す
19
+ */
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+
24
+ // ★`npx ... setup <鍵>` で呼ばれると argv[2] が 'setup' になる。★1つずらす
25
+ const 引数 = process.argv.slice(2).filter((x) => x !== 'setup');
26
+ const 鍵 = 引数[0] || process.env.BANTO_KEY || '';
27
+ if (!鍵) {
28
+ console.error('使い方: npx -y @horizon_works/banto setup "<鍵.署名>"');
29
+ process.exit(1);
30
+ }
31
+
32
+ // ★置き場を探す(★見つからなければ、いちばんありそうな所に作る)
33
+ function 設定の場所() {
34
+ if (引数[1]) return 引数[1];
35
+ if (process.env.BANTO_CONFIG) return process.env.BANTO_CONFIG;
36
+ const home = os.homedir();
37
+ const 候補 = [
38
+ path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
39
+ path.join(home, '.gemini', 'mcp_config.json'),
40
+ path.join(process.cwd(), 'mcp_config.json'),
41
+ ];
42
+ for (const p of 候補) { try { if (fs.existsSync(p)) return p; } catch (_) {} }
43
+ return 候補[0]; // ★無ければ、いちばん標準の所に作る
44
+ }
45
+
46
+ const P = 設定の場所();
47
+ const 印 = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
48
+
49
+ let j = { mcpServers: {} };
50
+ let 既にある = false;
51
+ if (fs.existsSync(P)) {
52
+ 既にある = true;
53
+ let s;
54
+ try { s = fs.readFileSync(P, 'utf8'); }
55
+ catch (e) { console.error('🚨 読めません: ' + e.message + '\n★何も書いていません。'); process.exit(1); }
56
+ // ★BOM を外してから読む(★これで落ちて、前の版は中身を消した)
57
+ s = s.replace(/^/, '');
58
+ try { j = JSON.parse(s); }
59
+ catch (e) {
60
+ console.error('🚨 いまの設定ファイルが読めませんでした(' + e.message + ')');
61
+ console.error(' ' + P);
62
+ console.error('★何も書いていません。★消さないために止めました。');
63
+ console.error('★このファイルを開いて、形が壊れていないかご確認ください。');
64
+ process.exit(1);
65
+ }
66
+ if (typeof j !== 'object' || j === null || Array.isArray(j)) {
67
+ console.error('🚨 設定の形が思っていたものと違います。★何も書いていません。');
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ j.mcpServers = j.mcpServers || {};
73
+ const 前からあった = Object.keys(j.mcpServers).filter((k) => k !== 'banto');
74
+
75
+ j.mcpServers.banto = {
76
+ command: 'npx',
77
+ args: ['-y', '@horizon_works/banto'],
78
+ env: { BANTO_KEY: 鍵 },
79
+ };
80
+
81
+ // ★控えを取ってから書く
82
+ if (既にある) {
83
+ try { fs.copyFileSync(P, P + '.bak-' + 印); }
84
+ catch (e) { console.error('🚨 控えが取れませんでした: ' + e.message + '\n★何も書いていません。'); process.exit(1); }
85
+ }
86
+ fs.mkdirSync(path.dirname(P), { recursive: true });
87
+ fs.writeFileSync(P, JSON.stringify(j, null, 2) + '\n', 'utf8');
88
+
89
+ // ★書いたと言う前に、読み直して確かめる
90
+ let 確認;
91
+ try { 確認 = JSON.parse(fs.readFileSync(P, 'utf8').replace(/^/, '')); }
92
+ catch (e) { console.error('🚨 書いたものが読めません: ' + e.message); process.exit(1); }
93
+
94
+ const 残っている = 前からあった.filter((k) => 確認.mcpServers && 確認.mcpServers[k]);
95
+ const 消えた = 前からあった.filter((k) => !(確認.mcpServers && 確認.mcpServers[k]));
96
+
97
+ console.log('✅ 書きました: ' + P);
98
+ console.log(' banto: ' + (確認.mcpServers.banto ? '入りました' : '❌ 入っていません'));
99
+ console.log(' 前からあったもの: ' + (前からあった.length ? 前からあった.join(', ') : '(なし)'));
100
+ console.log(' そのまま残った : ' + (残っている.length ? 残っている.join(', ') : '(なし)'));
101
+ if (消えた.length) {
102
+ console.error('🚨 消えたものがあります: ' + 消えた.join(', '));
103
+ console.error(' ★控えから戻せます: ' + P + '.bak-' + 印);
104
+ process.exit(1);
105
+ }
106
+ if (既にある) console.log(' 控え: ' + path.basename(P) + '.bak-' + 印);
107
+ console.log('\n★このあと、Antigravity を開き直してください。');
package/law_bridge.js ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * 法令パーツを、番頭の中から呼ぶ(★MCP in MCP)
3
+ *
4
+ * 🚨 由来: 2026-08-28。★法令パーツは別のMCPサーバーなので、棚に並ばなかった。
5
+ * ★「MCP in MCP できる」を**実測で確かめてから**この形にした
6
+ * (子を spawn → initialize → tools/list → tools/call が通ることを確認済み)。
7
+ *
8
+ * ★なぜ中継にするか
9
+ * ・利用者の設定は**1行のまま**(もう1つ登録してもらわない)
10
+ * ・鍵も**1つのまま**
11
+ * ・★限定の関所(entitlements)を**番頭が一括で見られる**
12
+ *
13
+ * ★見え方(2026-08-28 裁定=「見えるが使えない」)
14
+ * ・道具は**誰にでも見える**(契約していない方にも一覧に出る)
15
+ * ・呼ぶと「ご契約に含まれていません」と返る(★入口になる)
16
+ *
17
+ * ★重さ対策
18
+ * ・道具の定義は**ここに静的に持つ**(起動のたびに子を立てない)
19
+ * ・子を立てるのは**実際に呼ばれて、かつ契約がある**ときだけ
20
+ * ・一度立てたら**使い回す**(15分さわらなければ落とす)
21
+ * ・★静的な定義が子とズレていないかは `check_law_tools.mjs` が突き合わせる
22
+ */
23
+ const path = require('path');
24
+ const { spawn } = require('child_process');
25
+
26
+ const 名前 = ["law_search", "law_scene", "law_authority", "law_gaps"];
27
+ const SUPPLEMENT = 'law-parts';
28
+
29
+ // ★子の場所。★配布版では npm の別パッケージ、開発中はリポジトリの中
30
+ function 子の場所() {
31
+ if (process.env.LAW_PARTS_SERVER) return process.env.LAW_PARTS_SERVER;
32
+ const 候補 = [
33
+ path.join(__dirname, '..', 'law_parts', 'server.js'),
34
+ path.join(__dirname, 'law_parts', 'server.js'),
35
+ ];
36
+ const fs = require('fs');
37
+ for (const p of 候補) { try { if (fs.existsSync(p)) return p; } catch (_) {} }
38
+ return null;
39
+ }
40
+
41
+ const TOOLS = [
42
+ {
43
+ "name": "law_search",
44
+ "description": "仕事の場面(相談・就業規則・給与計算・手続)から、労働・社会保険の法令パーツを引く。★場面と根拠を分けて返す。★未解決の問い(open_questions_count)と、★このパーツでは判断できないこと(blocked_tasks)を必ず一緒に返すので、必ず読むこと。",
45
+ "inputSchema": {
46
+ "type": "object",
47
+ "properties": {
48
+ "q": {
49
+ "type": "string",
50
+ "description": "探したい語。空白区切りで複数可(例: 年休 時季変更)"
51
+ },
52
+ "limit": {
53
+ "type": "number",
54
+ "description": "場面・根拠それぞれの最大件数(既定6)"
55
+ }
56
+ },
57
+ "required": [
58
+ "q"
59
+ ]
60
+ }
61
+ },
62
+ {
63
+ "name": "law_scene",
64
+ "description": "場面パーツの全文を返す(answers / pitfalls / 根拠の一覧 / ★blocked_tasks / ★open_questions を含む)。law_search の scene_hits の id を渡す。",
65
+ "inputSchema": {
66
+ "type": "object",
67
+ "properties": {
68
+ "id": {
69
+ "type": "string",
70
+ "description": "場面ID(例: annual_leave)"
71
+ }
72
+ },
73
+ "required": [
74
+ "id"
75
+ ]
76
+ }
77
+ },
78
+ {
79
+ "name": "law_authority",
80
+ "description": "根拠(条文・省令・告示・通達・判例)の全文を返す。★読み替え(overridden_by)があるときは、読み替え側も一緒に返す。",
81
+ "inputSchema": {
82
+ "type": "object",
83
+ "properties": {
84
+ "id": {
85
+ "type": "string",
86
+ "description": "根拠ID(例: labor_law_39, case_52785)"
87
+ }
88
+ },
89
+ "required": [
90
+ "id"
91
+ ]
92
+ }
93
+ },
94
+ {
95
+ "name": "law_gaps",
96
+ "description": "★このパーツ集で「まだ埋まっていないこと」を返す。★何が入っているかより、★何が入っていないかを先に見るための口。",
97
+ "inputSchema": {
98
+ "type": "object",
99
+ "properties": {
100
+ "scene": {
101
+ "type": "string",
102
+ "description": "場面IDで絞る(省略時は全部)"
103
+ },
104
+ "status": {
105
+ "type": "string",
106
+ "description": "unresolved / partial(省略時は両方)"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ];
112
+
113
+ let 子 = null, 待ち = new Map(), 次のid = 1000, 最後に使った = 0, 掃除 = null;
114
+
115
+ function 子を立てる() {
116
+ const srv = 子の場所();
117
+ if (!srv) throw new Error('法令パーツの本体が見つかりません');
118
+ const p = spawn(process.execPath, [srv], { stdio: ['pipe', 'pipe', 'ignore'] });
119
+ let buf = '';
120
+ p.stdout.on('data', (d) => {
121
+ buf += d;
122
+ let i;
123
+ while ((i = buf.indexOf(String.fromCharCode(10))) >= 0) {
124
+ const line = buf.slice(0, i); buf = buf.slice(i + 1);
125
+ if (!line.trim()) continue;
126
+ try {
127
+ const j = JSON.parse(line);
128
+ const f = 待ち.get(j.id);
129
+ if (f) { 待ち.delete(j.id); f(j); }
130
+ } catch (_) {}
131
+ }
132
+ });
133
+ p.on('exit', () => { if (子 && 子.p === p) 子 = null; });
134
+ const 送る = (o) => p.stdin.write(JSON.stringify(o) + String.fromCharCode(10));
135
+ 送る({ jsonrpc: '2.0', id: 1, method: 'initialize',
136
+ params: { protocolVersion: '2024-11-05', capabilities: {},
137
+ clientInfo: { name: 'banto', version: '1' } } });
138
+ return { p, 送る };
139
+ }
140
+
141
+ function 呼ぶ(method, params, 秒 = 30) {
142
+ return new Promise((res, rej) => {
143
+ if (!子) 子 = 子を立てる();
144
+ 最後に使った = Date.now();
145
+ if (!掃除) {
146
+ // ★15分さわらなければ落とす(★立てっぱなしにしない)
147
+ 掃除 = setInterval(() => {
148
+ if (子 && Date.now() - 最後に使った > 15 * 60e3) { try { 子.p.kill(); } catch (_) {} 子 = null; }
149
+ }, 60e3);
150
+ if (掃除.unref) 掃除.unref();
151
+ }
152
+ const id = 次のid++;
153
+ const t = setTimeout(() => { 待ち.delete(id); rej(new Error('法令パーツが応答しません')); }, 秒 * 1000);
154
+ 待ち.set(id, (j) => { clearTimeout(t); j.error ? rej(new Error(j.error.message || '失敗')) : res(j.result); });
155
+ 子.送る({ jsonrpc: '2.0', id, method, params });
156
+ });
157
+ }
158
+
159
+ /** この名前は法令パーツのものか */
160
+ function これか(name) { return 名前.indexOf(name) >= 0; }
161
+
162
+ /**
163
+ * 呼ぶ(★契約を先に見る)
164
+ * @param 契約あり true なら通す
165
+ */
166
+ async function 中継(name, args, 契約あり) {
167
+ if (!契約あり) {
168
+ return {
169
+ verdict: '実行していません',
170
+ 理由: '★法令パーツはご契約に含まれていません。',
171
+ これは何か:
172
+ '労働・社会保険の「仕事の場面」から、条文・省令・告示・通達・判例を1手で引く道具です。'
173
+ + '★引用は原文と機械で全件照合しています。',
174
+ お問い合わせ: '★ご入用でしたら、番頭を出している者(ホライズンワークス)へお知らせください。',
175
+ };
176
+ }
177
+ const r = await 呼ぶ('tools/call', { name, arguments: args || {} });
178
+ const t = r && r.content && r.content[0] && r.content[0].text;
179
+ if (typeof t === 'string') { try { return JSON.parse(t); } catch (_) { return { text: t }; } }
180
+ return r;
181
+ }
182
+
183
+ module.exports = { TOOLS, これか, 中継, SUPPLEMENT, 名前 };
package/license/client.js CHANGED
@@ -98,10 +98,16 @@ function post(url, body, apiKey, timeoutMs = 4000) {
98
98
  apiKey ? { apikey: apiKey, Authorization: 'Bearer ' + apiKey } : {}
99
99
  ) },
100
100
  (res) => {
101
- let b = '';
102
- res.on('data', (d) => (b += d));
101
+ // 🚨★**バイトのまま溜めて、最後に1回だけ文字へ直す**(2026-08-28)。
102
+ // ★前は `b += d` で、届いたかたまりを**その場で文字に直していた**。
103
+ // ★★日本語は1文字3バイト。★かたまりの切れ目が文字の途中に来ると、
104
+ // ★そこだけ「」に化ける(★棚の説明で実際に起きた)。
105
+ // ★★通信は成功し、JSON も読めるので、**どこにも異常が出ない**。
106
+ const 粒 = [];
107
+ res.on('data', (d) => 粒.push(d));
108
+ const 読む = () => Buffer.concat(粒).toString('utf8');
103
109
  res.on('end', () => {
104
- try { resolve(JSON.parse(b)); } catch (e) { reject(new Error('応答を読めません')); }
110
+ try { resolve(JSON.parse(読む())); } catch (e) { reject(new Error('応答を読めません')); }
105
111
  });
106
112
  }
107
113
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@horizon_works/banto",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "AI番頭のOS。ルール・記憶・検査を1本のMCPで配ります。中身をクラウドへ渡さずに検査できます。",
5
5
  "keywords": [
6
6
  "mcp",
@@ -24,6 +24,8 @@
24
24
  },
25
25
  "files": [
26
26
  "server.js",
27
+ "law_bridge.js",
28
+ "add_to_config.js",
27
29
  "gate_core.js",
28
30
  "privacy.js",
29
31
  "shelf.js",
package/server.js CHANGED
@@ -1,4 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ // 🚨★**`setup` で呼ばれたら、MCPサーバーとしてではなく「設定を書く道具」として動く**
3
+ // (2026-08-28)。★由来=配った1行が Claude Code 前提で、Antigravity の方が入れられなかった。
4
+ // ★★**どのクライアントでも、ターミナルの1行で入る**ようにする:
5
+ // npx -y @horizon_works/banto setup "<鍵.署名>"
6
+ // ★既にある設定は消さない。★読めなければ書かない。★書いたら読み直して確かめる。
7
+ if (process.argv[2] === 'setup') {
8
+ require('./add_to_config.js');
9
+ return;
10
+ }
2
11
  /**
3
12
  * 番頭ゲート MCP サーバー(最小版 v0.1)
4
13
  *
@@ -221,6 +230,9 @@ function secondOpinion(targetPath) {
221
230
 
222
231
  // ---------------------------------------------------------------- 道具の定義
223
232
 
233
+ // ★法令パーツの橋(★無くても番頭は動く。★あれば道具が4つ増える)
234
+ const LAW = (() => { try { return require('./law_bridge.js'); } catch (_) { return null; } })();
235
+
224
236
  const TOOLS = [
225
237
  {
226
238
  name: 'check_file',
@@ -1176,11 +1188,17 @@ function supPost(fn, body) {
1176
1188
  apikey: anonKey, Authorization: 'Bearer ' + anonKey,
1177
1189
  },
1178
1190
  }, (res) => {
1179
- let b = '';
1180
- res.on('data', (d) => (b += d));
1191
+ // 🚨★**バイトのまま溜めて、最後に1回だけ文字へ直す**(2026-08-28)。
1192
+ // ★前は `b += d` で、届いたかたまりを**その場で文字に直していた**。
1193
+ // ★★日本語は1文字3バイト。★かたまりの切れ目が文字の途中に来ると、
1194
+ // ★そこだけ「」に化ける(★棚の説明で実際に起きた)。
1195
+ // ★★通信は成功し、JSON も読めるので、**どこにも異常が出ない**。
1196
+ const 粒 = [];
1197
+ res.on('data', (d) => 粒.push(d));
1198
+ const 読む = () => Buffer.concat(粒).toString('utf8');
1181
1199
  res.on('end', () => {
1182
1200
  let j;
1183
- try { j = JSON.parse(b); } catch (_) { return reject(new Error('応答を読めません')); }
1201
+ try { j = JSON.parse(読む()); } catch (_) { return reject(new Error('応答を読めません')); }
1184
1202
  if (res.statusCode >= 400) {
1185
1203
  return reject(new Error((j && (j.message || j.hint)) || ('HTTP ' + res.statusCode)));
1186
1204
  }
@@ -1542,7 +1560,21 @@ function 足あとを残す(name, args) {
1542
1560
 
1543
1561
  // ---------------------------------------------------------------- 道具の実行
1544
1562
 
1563
+ // ★その鍵が、そのサプリを契約しているか(★サーバーに聞く。手元では決めない)
1564
+ async function 契約あり(名前) {
1565
+ try {
1566
+ // ★取れれば契約あり。★取れなければ無い(★例外の中身では判定しない)
1567
+ const rows = await supPost('get_supplement', { p_name: 名前 });
1568
+ return Array.isArray(rows) && rows.length > 0;
1569
+ } catch (_) { return false; }
1570
+ }
1571
+
1545
1572
  function callTool(name, args) {
1573
+ // ★法令パーツ(別のMCP)への中継
1574
+ if (LAW && LAW.これか(name)) {
1575
+ return 契約あり(LAW.SUPPLEMENT).then((ok) => LAW.中継(name, args, ok));
1576
+ }
1577
+
1546
1578
  if (name === 'check_file') {
1547
1579
  const p = String(args && args.path || '');
1548
1580
  if (!p) throw new Error('path が指定されていません');
@@ -1773,7 +1805,10 @@ async function handle(req) {
1773
1805
  }
1774
1806
 
1775
1807
  if (method === 'tools/list') {
1776
- return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
1808
+ // ★法令パーツの道具も**誰にでも見せる**(2026-08-28 裁定)。
1809
+ // ★契約が無い方には、呼んだときに「含まれていません」と返る。
1810
+ // ★見えないと存在を知られないので、入口として見せる。
1811
+ return { jsonrpc: '2.0', id, result: { tools: TOOLS.concat(LAW ? LAW.TOOLS : []) } };
1777
1812
  }
1778
1813
 
1779
1814
  if (method === 'tools/call') {