@horizon_works/banto 0.5.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.
Files changed (36) hide show
  1. package/README.md +109 -0
  2. package/env.js +83 -0
  3. package/gate_core.js +629 -0
  4. package/gtasks.js +233 -0
  5. package/license/client.js +138 -0
  6. package/package.json +45 -0
  7. package/payload/compliance/SKILL.md +113 -0
  8. package/payload/compliance/dict/00_/345/213/225/344/275/234/347/242/272/350/252/215.json +14 -0
  9. package/payload/compliance/dict/06_house_rules.json +16 -0
  10. package/payload/compliance/dict/_sample/04_common_keihyo.json +270 -0
  11. package/payload/compliance/dict/_sample/README.md +72 -0
  12. package/payload/compliance/lint.js +265 -0
  13. package/payload/disciplines//347/225/252/351/240/255/343/201/256/344/275/234/346/263/225.md +64 -0
  14. package/payload/disciplines//350/207/252/345/267/261/345/201/245/350/250/272.md +74 -0
  15. package/payload/disciplines//351/200/261/346/254/241/343/203/241/343/203/263/343/203/206.md +60 -0
  16. package/payload/migration/protocol.md +35 -0
  17. package/payload/skill-maker/SKILL_base_anthropic.md +357 -0
  18. package/payload/skill-maker/skill-maker-v1.0.md +305 -0
  19. package/payload/skills/_/343/201/202/343/201/250/343/201/247/350/266/263/343/201/233/343/202/213/343/202/202/343/201/256.md +47 -0
  20. package/payload/skills/image-gen/SKILL.md +147 -0
  21. package/payload/skills/image-gen/gen.js +225 -0
  22. package/payload/skills/remember/SETUP_Google/351/200/243/346/220/272.md +106 -0
  23. package/payload/skills/remember/SKILL.md +158 -0
  24. package/payload/skills/remember/handoff.js +149 -0
  25. package/payload/skills/remember/tasks.js +320 -0
  26. package/payload/skills/slide-deck/SKILL.md +160 -0
  27. package/payload/skills/transcribe/README.md +182 -0
  28. package/payload/skills/transcribe/dict.json +10 -0
  29. package/payload/skills/transcribe/enroll_speaker.py +76 -0
  30. package/payload/skills/transcribe/identify_speakers.py +153 -0
  31. package/payload/skills/transcribe/transcribe.py +126 -0
  32. package/privacy.js +234 -0
  33. package/server.js +1671 -0
  34. package/shelf.js +172 -0
  35. package/subconscious.js +150 -0
  36. package/supplements.json +192 -0
package/gate_core.js ADDED
@@ -0,0 +1,629 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 番頭ゲート(フリー版 v1.0・確定版) — AIに中身を見せずに、AIに検査させる
4
+ *
5
+ * 株式会社ホライズンワークス
6
+ *
7
+ * ★この版で確定します(2026-08-20)。以後この配布版は更新しません。
8
+ * 道具は4つ全部入りです(check_file / check_text / remember / recall)。
9
+ * ★ここが検出と記録の「正本」で、社内版はこれを読み込んで辞書と鍵を足しているだけです。
10
+ * 直すときは、必ずこのファイルを直してください(2か所に持たない)。
11
+ *
12
+ * ★この道具の要
13
+ * ・check_file は「パス」だけを受け取る。ファイルを読むのは**このプログラム**で、AIではない
14
+ * ・AIへ返すのは「件数」と「判定」だけ。★本文・該当行・該当語は返さない
15
+ * ・詳細は手元のファイルへ書き、AIにはそのパスだけ渡す(開くのは人)
16
+ *
17
+ * ★辞書を同梱していません(利用者が自分で作ります)。
18
+ * 辞書が空でも「内部メモの検出」と「辞書に無い固有名の候補」は動きます。
19
+ *
20
+ * 依存ゼロ(MCP SDKを使わず、stdio上の改行区切りJSON-RPCを直接扱う)
21
+ */
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const os = require('os');
25
+ const readline = require('readline');
26
+
27
+ // ───────────────────────────────── 辞書(利用者が育てる)
28
+ function loadDict() {
29
+ // ★探す場所(2026-08-20 に __dirname を追加)。
30
+ // 由来=「はじめに.txt」は『このフォルダに置いてください』と案内しているのに、
31
+ // コードは cwd と ホームしか見ておらず、**案内どおりに置くと永久に見つからなかった**。
32
+ // 配った形(別フォルダへコピーして、別の作業フォルダから起動)で試して初めて分かった。
33
+ // ★手順は、自分の環境で成立していた理由まで確かめないと渡せない。
34
+ const candidates = [
35
+ process.env.BANTOU_GATE_DICT,
36
+ path.join(process.cwd(), 'bantou-gate-dict.json'), // 作業フォルダ
37
+ path.join(__dirname, 'bantou-gate-dict.json'), // ★server.js と同じ場所(案内している場所)
38
+ path.join(os.homedir(), '.bantou-gate-dict.json'), // 全部の仕事で共通に使うとき
39
+ ].filter(Boolean);
40
+ for (const p of candidates) {
41
+ try {
42
+ if (fs.existsSync(p)) {
43
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
44
+ const words = Array.isArray(j) ? j : (j.words || []);
45
+ return { path: p, words: words.filter((w) => typeof w === 'string' && w.length >= 2) };
46
+ }
47
+ } catch (_) {}
48
+ }
49
+ return { path: null, words: [] };
50
+ }
51
+
52
+ // ───────────────────────────────── 検査(辞書に依存しない2系統+辞書)
53
+ // 🚨2026-08-21 に絞り直した。前日に足した語が、**業務文書のふつうの言葉**を止めていた。
54
+ // 実例=給与の「上長承認待ち」(正規のステータス)/懲戒事例の「内部監査で発覚」。
55
+ // ★前日「年金の裁定請求」で気づいて『裁定』単独を外したのに、**同じ形の語を他に4つ残していた**。
56
+ // ★内部語として拾うのは「その言葉が業務文書に出てこないもの」だけ。
57
+ const INTERNAL_PATTERNS = [
58
+ { name: '未決の印', re: /(未決|決めきれ|先に決めて|決めてほしい)/g },
59
+ { name: '作業中メモ', re: /(TODO|ToDo|FIXME|WIP|仮置き|下書きメモ|検討メモ|暫定メモ|あとで直す|後で直す|書きかけ)/g },
60
+ { name: '内部注記', re: /(社外秘|配布禁止|部外秘|内部用のみ|置換対応表|内部台帳)/g },
61
+ { name: '決裁の記録', re: /(裁定済|主の裁定|主の決定|主の判断|主の指示)/g },
62
+ ];
63
+
64
+ // 人名+敬称 / 法人名 / 屋号 らしい形(★辞書は使わない)
65
+ //
66
+ // ★2026-08-20 に総入れ替え。それまでは
67
+ // ・一般語を落とす仕掛けが無く「課長」「お客様」「当事務所」でも鳴っていた
68
+ // ・行頭の1文字姓(「南さんに相談」)は2文字要求なので永久に届かなかった
69
+ // ・「氏名」を人名として拾っていた
70
+ // → 拾う側と落とす側を、両方いれた。
71
+ const NAMEISH = [
72
+ // 敬称の直前を見る。うしろの助詞(に・の・へ)は見ない
73
+ // ★「氏名」は敬称ではないので (?!名) で外す
74
+ [/[一-龥ぁ-んァ-ヴー]{1,5}(?=(?:先生|社長|代表|専務|常務|部長|課長|主任|様|さん|氏(?!名)))/g, '人名+敬称'],
75
+ [/(?:株式会社|合同会社|有限会社|一般社団法人|一般財団法人|公益社団法人|公益財団法人|特定非営利活動法人|社会福祉法人|医療法人|学校法人|宗教法人)[  ]?[^\s。、「」『』()()\[\]]{1,12}/g, '法人名'],
76
+ [/[^\s。、「」『』()()\[\]]{2,12}(?:株式会社|社労士法人|税理士法人|会計事務所|行政書士事務所|社会保険労務士事務所)/g, '法人名'],
77
+ [/[一-龥ァ-ヶ]{2,8}(?=事務所)/g, '事務所名'],
78
+ [/[一-龥ぁ-んァ-ヶ]{2,8}(?=(?:工務店|商店|工業所|製作所|鉄工所))/g, '屋号'],
79
+ ];
80
+
81
+ // ★落とす側。ここが無いと、ふつうの業務文書で鳴りっぱなしになり、読む気がなくなる
82
+ const IGNORE = new Set([
83
+ '所長', '代表', '担当', '担当者', '責任', '責任者', '管理者', '弊社', '当社', '御社', '貴社',
84
+ '対象', 'サンプル', '事務所', '当事務所', '連絡先', '取引先', '委託先', '提携先', '所属先',
85
+ 'この', 'その', 'あの', '当該', '各', '本', '株式会社', '合同会社', '有限会社', '法人',
86
+ '利用者', '受講者', '参加', '講師', '顧問', '顧客', '相手', '本人', '皆様', '皆さん',
87
+ 'お客', 'お客様', '運営', '事務', '名前', '氏名',
88
+ '社労士', '税理士', '行政書士', '弁護士', '会計士', '従業員', '社員', '職員', '労働者',
89
+ '過半数', '組合', '労働組合', '同一', '過去', '単一', '複数', '自社', '他社', '外部', '内部',
90
+ '一般', '専門', '双方', '当方', '先方', '記載', '該当', '所定',
91
+ ]);
92
+ // 1文字の漢字+敬称は苗字として拾うが、家族語・一般語は落とす
93
+ const ONE_CHAR_IGNORE = new Set([
94
+ '皆', '兄', '姉', '父', '母', '奥', '娘', '嫁', '婿', '孫', '客', 'お', 'ご',
95
+ '各', '同', '当', '本', '前', '次', '他', '別', '両', '全', '副', '元', '新', '旧',
96
+ ]);
97
+ const HIRAGANA_ONLY = /^[ぁ-ん]+$/;
98
+ const PARTICLE_END = /[はがのをにとへでもや、。::「『((]$/;
99
+ const SUFFIX_KANJI_TRAP = /[仕同多異模一様容態子]$/; // 仕様・同様・多様
100
+ const SAMA_COMPOUND = /^様[式子相態々]/; // 様式・様子・様相
101
+ const CODE_FRAGMENT = /[|()?:/\/、・\[\]{}]/;
102
+
103
+ function inspectText(text, dict) {
104
+ const lines = text.split(/\r?\n/);
105
+
106
+ // ① 辞書(利用者が入れた語)
107
+ const dictHits = [];
108
+ for (const w of dict.words) {
109
+ let n = 0;
110
+ for (const l of lines) { let i = 0; while ((i = l.indexOf(w, i)) !== -1) { n++; i += w.length; } }
111
+ if (n) dictHits.push({ word: w, count: n });
112
+ }
113
+
114
+ // ② 内部メモらしき記述(★辞書不要)
115
+ const internal = [];
116
+ lines.forEach((l, i) => {
117
+ for (const p of INTERNAL_PATTERNS) {
118
+ p.re.lastIndex = 0;
119
+ if (p.re.test(l)) internal.push({ line: i + 1, kind: p.name, text: l.trim().slice(0, 120) });
120
+ }
121
+ });
122
+
123
+ // ③ 辞書に無い固有名の候補(★辞書不要・目視の当たり付け)
124
+ const cand = new Map();
125
+ lines.forEach((l, i) => {
126
+ for (const [re, kind] of NAMEISH) {
127
+ re.lastIndex = 0;
128
+ let m;
129
+ while ((m = re.exec(l)) !== null) {
130
+ let s = m[0].trim();
131
+ const after = l.slice(m.index + m[0].length);
132
+ // 末尾の漢字・カタカナのひと続きが苗字(「信内で佐藤」→「佐藤」)
133
+ if (kind === '人名+敬称') {
134
+ const tail = s.match(/[一-龥ァ-ヶー]+$/);
135
+ if (tail) s = tail[0];
136
+ }
137
+ if (!s || IGNORE.has(s)) continue;
138
+ if (CODE_FRAGMENT.test(s)) continue;
139
+ if (s.length < 2) {
140
+ // 1文字は、漢字+敬称のときだけ苗字として拾う(南さん・東さん)
141
+ if (kind !== '人名+敬称' || !/^[一-龥]$/.test(s) || ONE_CHAR_IGNORE.has(s)) continue;
142
+ }
143
+ if (after.startsWith('様') && SUFFIX_KANJI_TRAP.test(s)) continue; // 仕様・同様・多様
144
+ if (SAMA_COMPOUND.test(after)) continue; // 様式・様子・様相
145
+ if (HIRAGANA_ONLY.test(s) && after.startsWith('さん')) continue; // たくさん・みなさん
146
+ if (PARTICLE_END.test(s)) continue;
147
+ if (dict.words.some((w) => s.includes(w))) continue; // 辞書で拾えているものは除く
148
+ const e = cand.get(s) || { count: 0, line: i + 1 };
149
+ e.count++; cand.set(s, e);
150
+ }
151
+ }
152
+ });
153
+ // 固有名は稀・一般語は頻出 → 出現の少ない順
154
+ const candidates = [...cand.entries()]
155
+ .map(([s, e]) => ({ text: s, count: e.count, line: e.line }))
156
+ .sort((a, b) => a.count - b.count);
157
+
158
+ return { dictHits, internal, candidates };
159
+ }
160
+
161
+ function report(targetLabel, r, dict) {
162
+ const lines = [];
163
+ lines.push('検査対象: ' + targetLabel);
164
+ lines.push('★このファイルはあなた(人)が手元で開くためのものです。AIはこの中身を読んでいません。');
165
+ lines.push('─'.repeat(60));
166
+ if (dict.path) lines.push('辞書: ' + dict.path + `(${dict.words.length}語)`);
167
+ else lines.push('辞書: ★未設定(辞書での検出は行っていません)');
168
+ lines.push('');
169
+ lines.push('■ 辞書の語');
170
+ r.dictHits.length ? r.dictHits.forEach((h) => lines.push(` ${h.word} ×${h.count}`))
171
+ : lines.push(' (なし)');
172
+ lines.push('');
173
+ lines.push('■ 内部メモらしき記述');
174
+ r.internal.length ? r.internal.forEach((h) => lines.push(` L${h.line} [${h.kind}] ${h.text}`))
175
+ : lines.push(' (なし)');
176
+ lines.push('');
177
+ lines.push('■ 辞書に無い固有名の候補(★検出ではなく、目視の当たり付け)');
178
+ r.candidates.length ? r.candidates.forEach((h) => lines.push(` L${h.line} ${h.text} ×${h.count}`))
179
+ : lines.push(' (なし)');
180
+ return lines.join('\n');
181
+ }
182
+
183
+ function summarize(r, dict, targetLabel) {
184
+ const reasons = [];
185
+ if (r.dictHits.length) reasons.push('辞書の語');
186
+ if (r.internal.length) reasons.push('内部メモらしき記述');
187
+ if (r.candidates.length) reasons.push('辞書に無い固有名の候補(要目視)');
188
+
189
+ let reportPath = null;
190
+ if (reasons.length) {
191
+ reportPath = path.join(os.tmpdir(), 'bantou_gate_' + Date.now() + '.txt');
192
+ try { fs.writeFileSync(reportPath, report(targetLabel, r, dict), 'utf8'); } catch (_) { reportPath = null; }
193
+ }
194
+
195
+ return {
196
+ dictionary_words: dict.words.length,
197
+ dictionary_hits: r.dictHits.reduce((s, h) => s + h.count, 0),
198
+ internal_note_count: r.internal.length,
199
+ dictionary_free_candidates: r.candidates.length,
200
+ verdict: reasons.length ? '要確認' : 'この検査では検出なし',
201
+ verdict_reasons: reasons,
202
+ detail_report: reportPath
203
+ ? '詳細は ' + reportPath + ' に書きました。★人が手元で開いてください(AIは読みません)'
204
+ : null,
205
+ note:
206
+ '★AIへ返しているのは件数と判定だけです。本文・該当行・該当語は返していません。' +
207
+ (dict.words.length === 0
208
+ ? ' ★辞書が未設定です。固有名の検出をするには辞書を作ってください(bantou-gate-dict.json)。'
209
+ : '') +
210
+ ' ★0件は「この検査では出なかった」であって、安全の証明ではありません。',
211
+ };
212
+ }
213
+
214
+ // ───────────────────────────────── 記録(学びを溜めて、ルールに落とす)
215
+ // ★入口は絶対に止めない。書きにくくすると、記録そのものが減る。
216
+ function logPath() {
217
+ return process.env.BANTOU_LOG || path.join(process.cwd(), 'bantou-log.md');
218
+ }
219
+ function stamp() {
220
+ const d = new Date(), p = (n) => String(n).padStart(2, '0');
221
+ return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
222
+ }
223
+ function remember(text, tag, sig) {
224
+ const t = String(text || '').trim();
225
+ if (!t) throw new Error('記録する内容がありません');
226
+ const f = logPath();
227
+ const oneLine = t.split(String.fromCharCode(10)).join(' ').split(String.fromCharCode(13)).join(' ');
228
+ const line = '- [ ] ' + stamp() + ' | ' + (tag || 'note')
229
+ + (sig ? ' 〔' + sig + '〕' : '') + ' | ' + oneLine + String.fromCharCode(10);
230
+ if (!fs.existsSync(f)) {
231
+ fs.writeFileSync(f,
232
+ '# 気づいたことの下書き帳' + String.fromCharCode(10) + String.fromCharCode(10)
233
+ + '> ★ここは途中の置き場です。正本ではありません。' + String.fromCharCode(10)
234
+ + '> ★同じ型が3件たまったら、ルールか手順に落としてください。それがゴールです。' + String.fromCharCode(10)
235
+ + '> 処理したものは [x] にしてください。' + String.fromCharCode(10) + String.fromCharCode(10), 'utf8');
236
+ }
237
+ fs.appendFileSync(f, line, 'utf8');
238
+ const all = fs.readFileSync(f, 'utf8').split(String.fromCharCode(10));
239
+ const open = all.filter((l) => l.indexOf('- [ ] ') === 0);
240
+ let sigCount = null;
241
+ if (sig) sigCount = all.filter((l) => l.indexOf('〔' + sig + '〕') !== -1).length;
242
+ const hint = (sigCount && sigCount >= 3)
243
+ ? '★この型は' + sigCount + '件目です。ルールか手順に落とす番です。'
244
+ : '';
245
+ return { saved_to: f, pending: open.length, sig: sig || null, sig_count: sigCount, hint: hint };
246
+ }
247
+ const ENTRY_RE = new RegExp('^- \\[( |x)\\] (\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}) \\| (.*)$');
248
+ const RULED_RE = new RegExp('^- (\\d{4}-\\d{2}-\\d{2}) 〔(.+?)〕 → (.*)$');
249
+ const THRESHOLD = 3; // ★何件たまったらルールにするか
250
+
251
+ // 記録を読んで、行番号つきで返す(★settle が狙い撃てるように)
252
+ function readLog() {
253
+ const f = logPath();
254
+ if (!fs.existsSync(f)) return { file: f, lines: [], entries: [], ruled: [] };
255
+ const lines = fs.readFileSync(f, 'utf8').split('\n');
256
+ const entries = [], ruled = [];
257
+ lines.forEach((l, i) => {
258
+ const m = l.match(ENTRY_RE);
259
+ if (m) {
260
+ const sigM = m[4].match(/〔(.+?)〕/);
261
+ entries.push({
262
+ id: i + 1, done: m[1] === 'x', date: m[2], time: m[3],
263
+ text: m[4], sig: sigM ? sigM[1] : null,
264
+ });
265
+ return;
266
+ }
267
+ const r = l.match(RULED_RE);
268
+ if (r) ruled.push({ date: r[1], sig: r[2], to: r[3] });
269
+ });
270
+ return { file: f, lines, entries, ruled };
271
+ }
272
+
273
+ function recall(days, onlyOpen) {
274
+ const { file, entries } = readLog();
275
+ const since = Date.now() - (Number(days) || 7) * 86400000;
276
+ const out = entries
277
+ .filter((e) => !(onlyOpen && e.done))
278
+ .filter((e) => new Date(e.date + 'T' + e.time).getTime() >= since)
279
+ .map((e) => ({ id: e.id, when: e.date + ' ' + e.time, done: e.done, text: e.text }));
280
+ return { entries: out.slice(-40), saved_to: file };
281
+ }
282
+
283
+ /**
284
+ * 溜まったものを型ごとに数えて、**次に何をするか**を返す。
285
+ * ★これが FL の山。溜めるのが目的ではなく、ルールか手順に落とすのが目的。
286
+ */
287
+ function review(record) {
288
+ const { file, entries, ruled } = readLog();
289
+ const ruledBy = {};
290
+ for (const r of ruled) ruledBy[r.sig] = r;
291
+
292
+ const bySig = {};
293
+ let noSig = 0;
294
+ for (const e of entries) {
295
+ if (!e.sig) { noSig++; continue; }
296
+ (bySig[e.sig] = bySig[e.sig] || []).push(e);
297
+ }
298
+
299
+ const toRule = [], notWorking = [], growing = [];
300
+ for (const [sig, list] of Object.entries(bySig)) {
301
+ const r = ruledBy[sig];
302
+ if (r) {
303
+ // ★ルールにした日より後に増えた分だけを数える(前の分まで数えると、書いた日に全部が再発になる)
304
+ const after = list.filter((e) => e.date > r.date);
305
+ if (after.length) notWorking.push({ sig, since_ruled: after.length, ruled_at: r.date, ruled_to: r.to, ids: after.map((e) => e.id) });
306
+ } else if (list.length >= THRESHOLD) {
307
+ toRule.push({ sig, count: list.length, ids: list.map((e) => e.id) });
308
+ } else {
309
+ growing.push({ sig, count: list.length });
310
+ }
311
+ }
312
+ toRule.sort((a, b) => b.count - a.count);
313
+
314
+ // ★振り返った日を控える(起動時の自己診断が「前回から◯日」を言えるように)
315
+ // 🚨 record を付けたときだけ。**起動時の自己診断も review を呼ぶので、
316
+ // 無条件に書くと「診断した=振り返った」ことになり、いつまでも「今日振り返り済み」になる。**
317
+ // 2026-08-21、実際にそうなった。★測る行為が、測る対象を変えていた。
318
+ if (record) {
319
+ const st = readState();
320
+ st.last_review = new Date().toISOString();
321
+ writeState(st);
322
+ }
323
+
324
+ const pending = entries.filter((e) => !e.done).length;
325
+ const next = [];
326
+ if (toRule.length) next.push('★' + toRule.length + 'つの型が' + THRESHOLD + '件を超えています。ルールか手順に落として、settle で印を付けてください');
327
+ if (notWorking.length) next.push('🚨 ルールにした後で、また同じことが起きています。ルールの文言でなく「どこで読まれなかったか」を疑ってください');
328
+ if (!next.length && pending) next.push('溜まっている分を読み直して、片づいたものは settle してください');
329
+ if (!next.length) next.push('いま片づけるものはありません');
330
+
331
+ return {
332
+ saved_to: file,
333
+ 総数: entries.length,
334
+ 未処理: pending,
335
+ 型を付けていないもの: noSig, // ★「型が無い」と「型を付けていない」は別
336
+ ルールにする番: toRule,
337
+ ルールが効いていない: notWorking,
338
+ 溜まりかけ: growing,
339
+ 次にやること: next,
340
+ note: '★' + THRESHOLD + '件たまったら、覚えておくのをやめて、手順か決まりごとにしてください。それがこの仕組みのゴールです。',
341
+ };
342
+ }
343
+
344
+ /**
345
+ * 片づける。★鳴った音を閉じる口。
346
+ * ids … 片づけた記録の番号(recall / review が返す id)
347
+ * ruled_to … ルールや手順にしたときの置き場所(sig と一緒に指定)
348
+ */
349
+ function settle(ids, sig, ruledTo) {
350
+ const { file, lines, entries } = readLog();
351
+ if (!fs.existsSync(file)) throw new Error('まだ記録がありません');
352
+ let closed = 0;
353
+ for (const id of (ids || [])) {
354
+ const i = Number(id) - 1;
355
+ if (i < 0 || i >= lines.length) continue;
356
+ if (lines[i].indexOf('- [ ] ') === 0) { lines[i] = lines[i].replace('- [ ] ', '- [x] '); closed++; }
357
+ }
358
+
359
+ let ruledLine = null;
360
+ if (sig && ruledTo) {
361
+ const d = stamp().slice(0, 10);
362
+ ruledLine = '- ' + d + ' 〔' + sig + '〕 → ' + ruledTo;
363
+ let at = lines.findIndex((l) => l.trim() === '## ルールにしたもの');
364
+ if (at === -1) {
365
+ lines.push('', '## ルールにしたもの', '', ruledLine, '');
366
+ } else {
367
+ lines.splice(at + 2, 0, ruledLine);
368
+ }
369
+ // ★その型の未処理を全部閉じる(ルールにしたのに開いたまま、を残さない)
370
+ for (const e of entries) {
371
+ if (e.sig === sig && !e.done && lines[e.id - 1].indexOf('- [ ] ') === 0) {
372
+ lines[e.id - 1] = lines[e.id - 1].replace('- [ ] ', '- [x] ');
373
+ closed++;
374
+ }
375
+ }
376
+ }
377
+
378
+ fs.writeFileSync(file, lines.join('\n'), 'utf8');
379
+ const after = readLog();
380
+ return {
381
+ saved_to: file,
382
+ 片づけた件数: closed,
383
+ ルールにした印: ruledLine,
384
+ 残りの未処理: after.entries.filter((e) => !e.done).length,
385
+ note: ruledLine
386
+ ? '★ルールにした日より後で同じことが起きたら、review が「ルールが効いていない」と出します。'
387
+ : '★片づけただけです。ルールにしたなら sig と ruled_to も渡してください。',
388
+ };
389
+ }
390
+
391
+ // ★「前回いつ振り返ったか」を控える。人に覚えさせない
392
+ function statePath() { return logPath().replace(/\.md$/i, '') + '.state.json'; }
393
+ function readState() {
394
+ try { return JSON.parse(fs.readFileSync(statePath(), 'utf8')); } catch (_) { return {}; }
395
+ }
396
+ function writeState(s) { try { fs.writeFileSync(statePath(), JSON.stringify(s, null, 2), 'utf8'); } catch (_) {} }
397
+
398
+ /**
399
+ * 起動のたびに、軽く自分を診る。
400
+ * ★MCPは常駐しないので、こちらから定期実行はできない。
401
+ * でも **instructions は起動のたびに作られる**ので、そこへ結果を載せれば
402
+ * 「番頭が自分から言い出す」形になる。人に覚えさせない。
403
+ * ★重い処理はしない(起動が遅くなる)。ファイル1本を読むだけ。
404
+ */
405
+ function selfDiagnosis() {
406
+ try {
407
+ const { entries, ruled } = readLog();
408
+ if (!entries.length) {
409
+ return '★まだ記録がありません。気づいたことがあったら「記録して」と言ってください(1行で構いません)。';
410
+ }
411
+ const r = review(false); // ★診断では印を付けない
412
+ const st = readState();
413
+ const lines = [];
414
+ const pending = r['未処理'];
415
+ const toRule = r['ルールにする番'].length;
416
+ const notWorking = r['ルールが効いていない'].length;
417
+
418
+ let days = null;
419
+ if (st.last_review) {
420
+ days = Math.floor((Date.now() - new Date(st.last_review).getTime()) / 86400000);
421
+ }
422
+
423
+ lines.push('溜まっているもの ' + pending + ' 件'
424
+ + (days === null ? '(★まだ一度も振り返っていません)'
425
+ : days >= 1 ? '/前回の振り返りから ' + days + ' 日' : '/今日振り返り済み'));
426
+ if (toRule) lines.push('★' + toRule + 'つの型が3件を超えています。**ルールか手順に落とす番**です');
427
+ if (notWorking) lines.push('🚨 ルールにした後で、また同じことが起きている型が ' + notWorking + ' つあります');
428
+ if (ruled.length) lines.push('これまでにルールにしたもの ' + ruled.length + ' 件');
429
+
430
+ const urge = (toRule || notWorking || pending >= 10 || (days !== null && days >= 7));
431
+ if (urge) lines.push('→ ★**今日のどこかで「振り返って」と言ってください**(review)');
432
+
433
+ return lines.join('\n');
434
+ } catch (_) {
435
+ return null; // ★診られなくても、番頭は動く。ここで止めない
436
+ }
437
+ }
438
+
439
+ /** 番頭がちゃんと入っているかの自己点検 */
440
+ function checkup() {
441
+ const { file, entries } = readLog();
442
+ const dict = loadDict();
443
+ const insPath = path.join(__dirname, 'instructions.md');
444
+ return {
445
+ 記録の置き場: file,
446
+ 記録がある: fs.existsSync(file),
447
+ 溜まっている数: entries.length,
448
+ 未処理: entries.filter((e) => !e.done).length,
449
+ 辞書: dict.path ? dict.path + '(' + dict.words.length + '語)' : '★未設定(固有名の検出は辞書なしの当たり付けだけ)',
450
+ 核の指示: fs.existsSync(insPath) ? '✅ あり' : '(このフォルダには無い)',
451
+ note: '★0件・未設定は異常ではありません。辞書は使いながら育てるものです。',
452
+ };
453
+ }
454
+
455
+ // ───────────────────────────────── 道具の定義
456
+ const TOOLS = [
457
+ {
458
+ name: 'check_file',
459
+ description:
460
+ 'ファイル(またはフォルダ)を検査し、外へ出してはいけない記述が無いかを見ます。' +
461
+ '★ファイルを読むのはこのプログラムであって、あなた(AI)ではありません。' +
462
+ '返るのは件数と判定だけで、本文・該当行・該当語は返りません。' +
463
+ '顧客名の入った下書きを、中身をクラウドへ渡さずに検査できます。',
464
+ inputSchema: {
465
+ type: 'object',
466
+ properties: { path: { type: 'string', description: '検査するファイルまたはフォルダのパス' } },
467
+ required: ['path'],
468
+ },
469
+ },
470
+ {
471
+ name: 'remember',
472
+ description:
473
+ '気づいたこと・失敗・学びを1行だけ記録します。★会話を止めずに使ってください(記録は促しであって関所ではありません)。' +
474
+ '同じ型キーが3件たまったら「ルールか手順に落とす番」だと返します。★溜めるのが目的ではありません。',
475
+ inputSchema: {
476
+ type: 'object',
477
+ properties: {
478
+ text: { type: 'string', description: '何が起きたか・なぜか(1行)' },
479
+ tag: { type: 'string', description: 'failure(失敗)/insight(発見)/value(価値観)/fact(事実)' },
480
+ sig: { type: 'string', description: '型キー。同じ失敗を数えるための短い名前(例: 確かめる前に断定した)' },
481
+ },
482
+ required: ['text'],
483
+ },
484
+ },
485
+ {
486
+ name: 'recall',
487
+ description: '最近の記録を引きます。★一日の終わりに見て、ルールに落とすものを選んでください。番号(id)が返るので settle で片づけられます。',
488
+ inputSchema: {
489
+ type: 'object',
490
+ properties: {
491
+ days: { type: 'number', description: '何日ぶんか(既定7)' },
492
+ only_open: { type: 'boolean', description: '未処理だけに絞る' },
493
+ },
494
+ },
495
+ },
496
+ {
497
+ name: 'review',
498
+ description:
499
+ '溜まったものを型ごとに数えて、★**次に何をするか**を返します。' +
500
+ '3件たまった型は「ルールにする番」、ルールにした後で再発した型は「ルールが効いていない」と出ます。' +
501
+ '★溜めるのが目的ではありません。手順か決まりごとにするのがゴールです。一日の終わりに呼んでください。',
502
+ inputSchema: { type: 'object', properties: {} },
503
+ },
504
+ {
505
+ name: 'settle',
506
+ description:
507
+ '片づけた記録に印を付けます。★鳴った音を閉じる口です。' +
508
+ 'ルールや手順に落としたときは sig と ruled_to を渡してください(その型の未処理もまとめて閉じます)。',
509
+ inputSchema: {
510
+ type: 'object',
511
+ properties: {
512
+ ids: { type: 'array', items: { type: 'number' }, description: '片づけた記録の番号(recall / review が返す id)' },
513
+ sig: { type: 'string', description: 'ルールにした型キー' },
514
+ ruled_to: { type: 'string', description: 'どこに書いたか(ファイル名・手順書の名前など)' },
515
+ },
516
+ },
517
+ },
518
+ {
519
+ name: 'checkup',
520
+ description: '番頭がちゃんと入っているかを自己点検します。記録の置き場・辞書・核の指示を見ます。',
521
+ inputSchema: { type: 'object', properties: {} },
522
+ },
523
+ {
524
+ name: 'check_text',
525
+ description:
526
+ '渡された文字列を検査します。★この道具は文字列を引数に取るので、その文字列はあなたの文脈に入ります。' +
527
+ '機密を含む可能性があるものは check_file を使ってください。',
528
+ inputSchema: {
529
+ type: 'object',
530
+ properties: { text: { type: 'string', description: '検査する文字列' } },
531
+ required: ['text'],
532
+ },
533
+ },
534
+ ];
535
+
536
+ function walk(p, acc, depth) {
537
+ const st = fs.statSync(p);
538
+ if (st.isDirectory()) {
539
+ if (depth > 6) return acc;
540
+ for (const e of fs.readdirSync(p)) {
541
+ if (e === 'node_modules' || e.startsWith('.git')) continue;
542
+ try { walk(path.join(p, e), acc, depth + 1); } catch (_) {}
543
+ }
544
+ } else if (/\.(md|txt|json|ya?ml|csv|html?|js|ts|py|sql)$/i.test(p) && st.size < 5 * 1024 * 1024) {
545
+ acc.push(p);
546
+ }
547
+ return acc;
548
+ }
549
+
550
+ function callTool(name, args) {
551
+ const dict = loadDict();
552
+ if (name === 'check_file') {
553
+ const p = String((args && args.path) || '');
554
+ if (!p) throw new Error('path が指定されていません');
555
+ if (!fs.existsSync(p)) throw new Error('見つかりません: ' + p);
556
+ const files = walk(p, [], 0);
557
+ if (!files.length) throw new Error('検査できるファイルがありません: ' + p);
558
+ const merged = { dictHits: [], internal: [], candidates: [] };
559
+ for (const f of files) {
560
+ const r = inspectText(fs.readFileSync(f, 'utf8'), dict);
561
+ merged.dictHits.push(...r.dictHits);
562
+ merged.internal.push(...r.internal);
563
+ merged.candidates.push(...r.candidates);
564
+ }
565
+ const out = summarize(merged, dict, p);
566
+ out.files_scanned = files.length;
567
+ return out;
568
+ }
569
+ if (name === 'remember') {
570
+ return remember(args && args.text, args && args.tag, args && args.sig);
571
+ }
572
+ if (name === 'recall') {
573
+ return recall(args && args.days, args && args.only_open);
574
+ }
575
+ if (name === 'review') return review(true); // ★人が呼んだときだけ印を付ける
576
+ if (name === 'settle') {
577
+ return settle(args && args.ids, args && args.sig, args && args.ruled_to);
578
+ }
579
+ if (name === 'checkup') return checkup();
580
+ if (name === 'check_text') {
581
+ const text = String((args && args.text) || '');
582
+ const out = summarize(inspectText(text, dict), dict, '(渡された文字列)');
583
+ out.files_scanned = 0;
584
+ return out;
585
+ }
586
+ throw new Error('未知の道具: ' + name);
587
+ }
588
+
589
+ // ───────────────────────────────── JSON-RPC
590
+ const send = (m) => process.stdout.write(JSON.stringify(m) + '\n');
591
+
592
+ function handle(req) {
593
+ const { id, method, params } = req;
594
+ if (method === 'initialize') {
595
+ return { jsonrpc: '2.0', id, result: {
596
+ protocolVersion: (params && params.protocolVersion) || '2025-06-18',
597
+ capabilities: { tools: {} },
598
+ serverInfo: { name: 'bantou-gate-free', version: '1.0.0' },
599
+ } };
600
+ }
601
+ if (method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
602
+ if (method === 'tools/call') {
603
+ try {
604
+ const out = callTool(params && params.name, params && params.arguments);
605
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] } };
606
+ } catch (e) {
607
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: 'エラー: ' + e.message }], isError: true } };
608
+ }
609
+ }
610
+ if (method === 'ping') return { jsonrpc: '2.0', id, result: {} };
611
+ if (id === undefined) return null;
612
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: ' + method } };
613
+ }
614
+
615
+ // ★このファイルは「正本」。社内版はこれを読み込んで、辞書と鍵を足すだけにする。
616
+ // (検出のロジックを2か所に持たないため)
617
+ module.exports = { inspectText, summarize, report, loadDict, walk, remember, recall, review, settle, checkup, readLog, selfDiagnosis, INTERNAL_PATTERNS, NAMEISH };
618
+
619
+ // 直接起動されたときだけ、MCPサーバーとして動く
620
+ if (require.main === module) {
621
+ readline.createInterface({ input: process.stdin }).on('line', (line) => {
622
+ const s = line.trim();
623
+ if (!s) return;
624
+ let req;
625
+ try { req = JSON.parse(s); } catch (_) { return; }
626
+ const res = handle(req);
627
+ if (res) send(res);
628
+ });
629
+ }