@clubs.place/cli 0.1.24 → 0.1.26
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/.env.example +8 -2
- package/PROVENANCE.json +28 -13
- package/README.md +1 -1
- package/bin/clubs.mjs +205 -23
- package/package.json +1 -1
- package/src/config.mjs +34 -11
- package/src/config.test.mjs +83 -0
- package/src/generator-env.mjs +42 -0
- package/src/generator-env.test.mjs +66 -0
- package/src/generators/hermes.mjs +188 -0
- package/src/generators/hermes.test.mjs +287 -0
- package/src/hermes-cron-script.test.mjs +105 -0
- package/src/hermes-cron.live.test.mjs +97 -0
- package/src/hermes-cron.mjs +421 -0
- package/src/hermes-cron.test.mjs +430 -0
- package/src/init.pipeline.test.mjs +130 -0
- package/src/narrative.hermes.test.mjs +74 -0
- package/src/narrative.mjs +30 -5
- package/src/narrative.targets.test.mjs +26 -0
- package/src/pack.test.mjs +23 -0
- package/src/profiles.mjs +25 -1
- package/src/profiles.test.mjs +15 -0
- package/src/prompt.mjs +1 -0
- package/src/prompt.test.mjs +5 -0
- package/src/sources/hermes.mjs +18 -9
- package/src/sources/hermes.test.mjs +129 -0
- package/src/tick.pipeline.test.mjs +166 -0
- package/src/tick.profiles.test.mjs +131 -0
- package/src/mcp.mjs +0 -286
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, openSync, closeSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { buildTickScript, shQuote } from "./hermes-cron.mjs";
|
|
8
|
+
|
|
9
|
+
const BIN = new URL("../bin/clubs.mjs", import.meta.url).pathname;
|
|
10
|
+
const narrative = { workBlocks: [{ title: "test", text: "test", metric: "1" }], handoff: { title: "t", text: "t" }, insights: [], selfNote: "t" };
|
|
11
|
+
|
|
12
|
+
const run = (command, args, env) => new Promise((resolve, reject) => {
|
|
13
|
+
const output = join(env.HOME, "process-output.txt");
|
|
14
|
+
const fd = openSync(output, "w");
|
|
15
|
+
const child = spawn(command, args, { env, stdio: ["ignore", fd, fd] });
|
|
16
|
+
closeSync(fd);
|
|
17
|
+
const timer = setTimeout(() => { child.kill("SIGKILL"); }, 20_000);
|
|
18
|
+
child.on("error", reject);
|
|
19
|
+
child.on("close", (code) => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
resolve({ code, out: readFileSync(output, "utf8") });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 実 tick + fanout + collector + generator。通信だけを preload でスタブ化し、
|
|
26
|
+
// 認証・会話・report はすべて temp 配下の架空データに限定する。
|
|
27
|
+
const fixture = (t, { native = false, generator } = {}) => {
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), "nippo-profiles-"));
|
|
29
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
30
|
+
const home = join(dir, "clubs");
|
|
31
|
+
const childHome = join(home, "agents", "child");
|
|
32
|
+
const homes = [join(dir, "A"), join(dir, "B")];
|
|
33
|
+
for (const [i, h] of homes.entries()) {
|
|
34
|
+
mkdirSync(join(h, "sessions"), { recursive: true });
|
|
35
|
+
writeFileSync(join(h, "sessions", "session_20261028_120000.json"), JSON.stringify({
|
|
36
|
+
session_id: "s", source: "cli", session_start: "2026-10-28T12:00:00Z",
|
|
37
|
+
last_updated: "2026-10-28T12:00:00Z",
|
|
38
|
+
messages: [
|
|
39
|
+
{ role: "assistant", tool_calls: [{ id: "c1", function: { name: "terminal", arguments: JSON.stringify({ command: i ? "CHILD_ONLY_COMMAND" : "ROOT_ONLY_COMMAND" }) } }] },
|
|
40
|
+
{ role: "tool", tool_call_id: "c1", name: "terminal", content: '{"ok":true}' },
|
|
41
|
+
],
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
for (const [i, h] of [home, childHome].entries()) {
|
|
45
|
+
mkdirSync(h, { recursive: true });
|
|
46
|
+
writeFileSync(join(h, ".env"), [
|
|
47
|
+
"SOURCE=hermes", "TZ_OFFSET_MIN=0",
|
|
48
|
+
"NIPPO_INGEST_URL=https://clubs.invalid/api/nippo/ingest",
|
|
49
|
+
`NIPPO_INGEST_TOKEN=${i ? "child-token" : "root-token"}`,
|
|
50
|
+
// native root は wrapper の既定 A を使う。子の明示 B はそれに勝つ。
|
|
51
|
+
...(!native || i ? [`HERMES_HOME=${homes[i]}`] : []),
|
|
52
|
+
`OPENROUTER_API_KEY=${i ? "child-key" : "root-key"}`,
|
|
53
|
+
].join("\n") + "\n");
|
|
54
|
+
}
|
|
55
|
+
const log = join(dir, "calls.jsonl");
|
|
56
|
+
const preload = join(dir, "stub.mjs");
|
|
57
|
+
writeFileSync(preload, `
|
|
58
|
+
import { appendFileSync } from "node:fs";
|
|
59
|
+
globalThis.fetch = async (url, options = {}) => {
|
|
60
|
+
const record = { url: String(url), home: process.env.CLUBS_NIPPO_HOME, auth: options.headers?.authorization ?? options.headers?.Authorization, body: options.body };
|
|
61
|
+
appendFileSync(${JSON.stringify(log)}, JSON.stringify(record) + "\\n");
|
|
62
|
+
if (String(url).endsWith("/collector-config")) return new Response(JSON.stringify({
|
|
63
|
+
schedule: { enabled: true }, due: true, reportDate: "2026-10-28", sources: ["hermes"],
|
|
64
|
+
generator: process.env.CLUBS_NIPPO_CHILD && !${native} ? { backend: "openrouter", model: "child/model" } : { backend: "hermes-cli" }
|
|
65
|
+
}));
|
|
66
|
+
if (String(url).includes("openrouter")) return new Response(JSON.stringify({
|
|
67
|
+
choices: [{ message: { content: ${JSON.stringify(JSON.stringify(narrative))} } }]
|
|
68
|
+
}));
|
|
69
|
+
if (String(url).endsWith("/ingest")) return new Response("{}");
|
|
70
|
+
throw new Error("Unexpected request: " + url);
|
|
71
|
+
};
|
|
72
|
+
`);
|
|
73
|
+
const hermes = join(dir, "hermes");
|
|
74
|
+
writeFileSync(hermes, `#!/bin/sh
|
|
75
|
+
cat > "$CLUBS_NIPPO_HOME/prompt.txt"
|
|
76
|
+
printf '{"generator":"hermes","home":"%s","hermesHome":"%s","key":"%s"}\\n' "$CLUBS_NIPPO_HOME" "$HERMES_HOME" "$OPENROUTER_API_KEY" >> ${shQuote(log)}
|
|
77
|
+
printf '%s' ${shQuote(JSON.stringify(narrative))}
|
|
78
|
+
`, { mode: 0o755 });
|
|
79
|
+
const env = {
|
|
80
|
+
PATH: process.env.PATH, HOME: dir, CLUBS_NIPPO_HOME: home,
|
|
81
|
+
HERMES_BIN: hermes, NODE_OPTIONS: `--import=${preload}`,
|
|
82
|
+
...(generator ? { GENERATOR: generator } : {}),
|
|
83
|
+
};
|
|
84
|
+
return { dir, home, childHome, homes, env, log, calls: () => readFileSync(log, "utf8").trim().split("\n").map(JSON.parse) };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const explicit of [false, true]) {
|
|
88
|
+
test(`root+child tick: ${explicit ? "真の GENERATOR 上書きを継承" : "root の導出 GENERATOR より子の OpenRouter 設定が優先"}`, async (t) => {
|
|
89
|
+
const f = fixture(t, { generator: explicit ? "hermes" : undefined });
|
|
90
|
+
const r = await run(process.execPath, [BIN, "tick"], f.env);
|
|
91
|
+
assert.equal(r.code, 0, r.out);
|
|
92
|
+
const calls = f.calls();
|
|
93
|
+
const generators = calls.filter((c) => c.generator || c.url.includes("openrouter"));
|
|
94
|
+
assert.equal(generators.length, 2, r.out);
|
|
95
|
+
assert.equal(generators[0].hermesHome, f.homes[0]);
|
|
96
|
+
assert.equal(generators[1].home, f.childHome);
|
|
97
|
+
if (explicit) {
|
|
98
|
+
assert.equal(generators[1].generator, "hermes");
|
|
99
|
+
assert.equal(generators[1].hermesHome, f.homes[1]);
|
|
100
|
+
} else {
|
|
101
|
+
assert.match(r.out, /generator=openrouter/);
|
|
102
|
+
assert.equal(JSON.parse(generators[1].body).model, "child/model");
|
|
103
|
+
assert.equal(generators[1].auth, "Bearer child-key");
|
|
104
|
+
}
|
|
105
|
+
assert.equal(calls.filter((c) => c.url?.endsWith("/ingest")).length, 2);
|
|
106
|
+
const childCalls = JSON.stringify(calls.filter((c) => c.home === f.childHome));
|
|
107
|
+
assert.ok(!childCalls.includes("root-key"));
|
|
108
|
+
assert.ok(!childCalls.includes("root-token"));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test("native cron wrapper: 所有者 A と child.env B の collect + generation を分離", async (t) => {
|
|
113
|
+
const f = fixture(t, { native: true });
|
|
114
|
+
const script = join(f.dir, "tick.sh");
|
|
115
|
+
writeFileSync(script, buildTickScript({
|
|
116
|
+
home: f.home, hermesHome: f.homes[0],
|
|
117
|
+
tickCmd: `${shQuote(process.execPath)} ${shQuote(BIN)} tick --only=hermes`,
|
|
118
|
+
logPath: join(f.dir, "cron.log"),
|
|
119
|
+
}));
|
|
120
|
+
const r = await run("bash", [script], { ...f.env, HERMES_HOME: f.homes[0] });
|
|
121
|
+
assert.equal(r.code, 0, r.out);
|
|
122
|
+
const generated = f.calls().filter((c) => c.generator);
|
|
123
|
+
assert.equal(generated.length, 2, r.out);
|
|
124
|
+
for (const [i, c] of generated.entries()) {
|
|
125
|
+
assert.equal(c.hermesHome, f.homes[i]);
|
|
126
|
+
assert.equal(c.key, i ? "child-key" : "root-key");
|
|
127
|
+
const prompt = readFileSync(join(c.home, "prompt.txt"), "utf8");
|
|
128
|
+
assert.ok(prompt.includes(i ? "CHILD_ONLY_COMMAND" : "ROOT_ONLY_COMMAND"));
|
|
129
|
+
assert.ok(!prompt.includes(i ? "ROOT_ONLY_COMMAND" : "CHILD_ONLY_COMMAND"));
|
|
130
|
+
}
|
|
131
|
+
});
|
package/src/mcp.mjs
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLUBS MCP サーバー(stdio・同一バイナリ同梱=全体設計 §6 / P0 §9)。
|
|
3
|
-
*
|
|
4
|
-
* `clubs-nippo mcp` で起動し、Claude Code / Cursor / Gemini CLI 等の
|
|
5
|
-
* MCP クライアントから CLUBS への登録・疎通確認を対話の中で完結させる。
|
|
6
|
-
*
|
|
7
|
-
* CTO裁定(clubsx#53 D-4)に従う:
|
|
8
|
-
* - ツールは clubs_register / clubs_link_status の2本のみ。
|
|
9
|
-
* 発行系・日報の書き込み系は載せない(B-1: 自己申告の発行経路を作らない。
|
|
10
|
-
* 日報の取り込みは従来どおり収集器の pull 経路のみ)。
|
|
11
|
-
* - 出力はサーバ確定値・ローカル設定の定型フィールドのみ(自由文・他ユーザー
|
|
12
|
-
* 由来のテキストを返さない=注入面の最小化)。
|
|
13
|
-
*
|
|
14
|
-
* **終わる(CLUBS 判断 #26・2026-10-16)**: AI を CLUBS につなぐのはリモート MCP
|
|
15
|
-
* (接続画面の URL+許可)になり、この CLI は日報の自動化専用になった。前に
|
|
16
|
-
* 登録した人の AI が起動したとき黙って落ちないよう、しばらくは動かしたまま、
|
|
17
|
-
* 起動・道具の説明・返事のすべてで「つなぎ直して」を伝える(DEPRECATION)。
|
|
18
|
-
*
|
|
19
|
-
* 依存ゼロ方針(このリポの流儀)のため SDK は使わず、MCP の stdio
|
|
20
|
-
* トランスポート(JSON-RPC 2.0・1行1メッセージ)を最小実装する。
|
|
21
|
-
* stdout はプロトコル専用。ログは必ず stderr へ。
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { createInterface } from "node:readline";
|
|
25
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
26
|
-
import { join } from "node:path";
|
|
27
|
-
import { APP_HOME, ROOT } from "./config.mjs";
|
|
28
|
-
import { collectorConfigUrl } from "./remote-config.mjs";
|
|
29
|
-
import { readHistory } from "./history.mjs";
|
|
30
|
-
|
|
31
|
-
const PROTOCOL_FALLBACK = "2025-06-18";
|
|
32
|
-
|
|
33
|
-
/** 終わることの知らせ。決まった文だけ(自由文を返さない方針のまま)。 */
|
|
34
|
-
export const DEPRECATION =
|
|
35
|
-
"この手元の MCP(clubs_register / clubs_link_status)は終わります。" +
|
|
36
|
-
"CLUBS の接続画面 https://clubs.place/connections からリモート MCP につなぎ直し、" +
|
|
37
|
-
"この登録は `claude mcp remove clubs` などで外してください。日報の自動化(tick)はこのまま動きます。";
|
|
38
|
-
|
|
39
|
-
const pkgVersion = () => {
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
|
|
42
|
-
} catch {
|
|
43
|
-
return "0.0.0";
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// ── 登録先 URL のガード(注入面対策) ─────────────────────
|
|
48
|
-
// MCP 経由の register は AI の文脈から呼ばれるため、細工された取込URLで
|
|
49
|
-
// トークンや日報の送り先を挿げ替えられないよう、既定で CLUBS のホストに限る。
|
|
50
|
-
// 自前ホストの開発者は CLUBS_NIPPO_ALLOW_HOST=<host> で1件だけ追加できる。
|
|
51
|
-
const urlAllowed = (raw) => {
|
|
52
|
-
try {
|
|
53
|
-
const u = new URL(raw);
|
|
54
|
-
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
55
|
-
if (local) return true; // ローカル開発
|
|
56
|
-
if (u.protocol !== "https:") return false;
|
|
57
|
-
if (u.hostname === "clubs.place" || u.hostname.endsWith(".clubs.place"))
|
|
58
|
-
return true;
|
|
59
|
-
return u.hostname === process.env.CLUBS_NIPPO_ALLOW_HOST;
|
|
60
|
-
} catch {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/** init コマンドと同じ書式で .env を更新(既存キーは生かす)。 */
|
|
66
|
-
const writeEnvUpdates = (updates) => {
|
|
67
|
-
mkdirSync(join(APP_HOME, "out"), { recursive: true });
|
|
68
|
-
const envPath = join(APP_HOME, ".env");
|
|
69
|
-
const lines = existsSync(envPath)
|
|
70
|
-
? readFileSync(envPath, "utf8").replace(/\n$/, "").split("\n")
|
|
71
|
-
: [
|
|
72
|
-
"# clubs-nippo の設定。キーの一覧は https://www.npmjs.com/package/clubs-nippo を参照。",
|
|
73
|
-
];
|
|
74
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
75
|
-
const row = `${key}=${value}`;
|
|
76
|
-
const idx = lines.findIndex((l) => l.match(new RegExp(`^\\s*${key}\\s*=`)));
|
|
77
|
-
if (idx >= 0) lines[idx] = row;
|
|
78
|
-
else lines.push(row);
|
|
79
|
-
}
|
|
80
|
-
writeFileSync(envPath, lines.join("\n") + "\n", "utf8");
|
|
81
|
-
return envPath;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const withTimeout = async (fn, ms) => {
|
|
85
|
-
const ctrl = new AbortController();
|
|
86
|
-
const timer = setTimeout(() => ctrl.abort(), ms);
|
|
87
|
-
try {
|
|
88
|
-
return await fn(ctrl.signal);
|
|
89
|
-
} finally {
|
|
90
|
-
clearTimeout(timer);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/** 連携トークンの実在確認=collector-config にサーバ照会する。 */
|
|
95
|
-
const serverCheck = async (ingestUrl, ingestToken) => {
|
|
96
|
-
const url = collectorConfigUrl(ingestUrl);
|
|
97
|
-
if (!url || !ingestToken) return { reachable: false, token_valid: false };
|
|
98
|
-
try {
|
|
99
|
-
const res = await withTimeout(
|
|
100
|
-
(signal) =>
|
|
101
|
-
fetch(url, {
|
|
102
|
-
signal,
|
|
103
|
-
headers: { Authorization: `Bearer ${ingestToken}` },
|
|
104
|
-
}),
|
|
105
|
-
10_000,
|
|
106
|
-
);
|
|
107
|
-
return { reachable: true, token_valid: res.ok };
|
|
108
|
-
} catch {
|
|
109
|
-
return { reachable: false, token_valid: false };
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// ── ツール定義 ────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
const TOOLS = [
|
|
116
|
-
{
|
|
117
|
-
name: "clubs_register",
|
|
118
|
-
description:
|
|
119
|
-
"【終わります】" +
|
|
120
|
-
DEPRECATION +
|
|
121
|
-
" / この端末を CLUBS に登録する(clubs-nippo init と同じ設定を書く)。url と token は CLUBS の接続画面(/nippo/connections)が発行した値をそのまま渡すこと。登録後にサーバへ疎通確認する。",
|
|
122
|
-
inputSchema: {
|
|
123
|
-
type: "object",
|
|
124
|
-
properties: {
|
|
125
|
-
url: {
|
|
126
|
-
type: "string",
|
|
127
|
-
description: "取込URL(接続画面が発行。…/api/nippo/ingest)",
|
|
128
|
-
},
|
|
129
|
-
token: { type: "string", description: "連携トークン(接続画面が発行)" },
|
|
130
|
-
source: {
|
|
131
|
-
type: "string",
|
|
132
|
-
description: "収集元(例: claude-code / cowork。省略可)",
|
|
133
|
-
},
|
|
134
|
-
agent_name: { type: "string", description: "体の表示名(省略可)" },
|
|
135
|
-
owner_name: { type: "string", description: "オーナー名(省略可)" },
|
|
136
|
-
},
|
|
137
|
-
required: ["url", "token"],
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "clubs_link_status",
|
|
142
|
-
description:
|
|
143
|
-
"【終わります】" +
|
|
144
|
-
DEPRECATION +
|
|
145
|
-
" / CLUBS との連携状態を返す(設定の有無・サーバ疎通・トークン有効性・ローカル最終記録日)。",
|
|
146
|
-
inputSchema: { type: "object", properties: {} },
|
|
147
|
-
},
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
export const callTool = async (name, args = {}) => ({
|
|
151
|
-
deprecated: DEPRECATION,
|
|
152
|
-
...(await callToolInner(name, args)),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const callToolInner = async (name, args = {}) => {
|
|
156
|
-
if (name === "clubs_register") {
|
|
157
|
-
const url = String(args.url ?? "");
|
|
158
|
-
const token = String(args.token ?? "");
|
|
159
|
-
if (!url || !token) {
|
|
160
|
-
return { ok: false, error: "url と token は必須です" };
|
|
161
|
-
}
|
|
162
|
-
let parsed;
|
|
163
|
-
try {
|
|
164
|
-
parsed = new URL(url);
|
|
165
|
-
} catch {
|
|
166
|
-
return { ok: false, error: "url が不正です" };
|
|
167
|
-
}
|
|
168
|
-
if (!/\/api\/nippo\/ingest\/?$/.test(parsed.pathname)) {
|
|
169
|
-
return {
|
|
170
|
-
ok: false,
|
|
171
|
-
error: "url は接続画面が発行した取込URL(…/api/nippo/ingest)を渡してください",
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (!urlAllowed(url)) {
|
|
175
|
-
return {
|
|
176
|
-
ok: false,
|
|
177
|
-
error:
|
|
178
|
-
"この url のホストには登録できません(clubs.place / localhost のみ。自前ホストは CLUBS_NIPPO_ALLOW_HOST で許可)",
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const updates = Object.fromEntries(
|
|
182
|
-
Object.entries({
|
|
183
|
-
NIPPO_INGEST_URL: url,
|
|
184
|
-
NIPPO_INGEST_TOKEN: token,
|
|
185
|
-
SOURCE: args.source,
|
|
186
|
-
AGENT_NAME: args.agent_name,
|
|
187
|
-
OWNER_NAME: args.owner_name,
|
|
188
|
-
}).filter(([, v]) => v),
|
|
189
|
-
);
|
|
190
|
-
const envPath = writeEnvUpdates(updates);
|
|
191
|
-
// 同一セッション内の link_status が新しい値を見られるように反映する
|
|
192
|
-
// (config.mjs の loadDotEnv は起動時に未設定キーしか埋めない)
|
|
193
|
-
for (const [key, value] of Object.entries(updates)) process.env[key] = value;
|
|
194
|
-
const server = await serverCheck(url, token);
|
|
195
|
-
return {
|
|
196
|
-
ok: server.token_valid,
|
|
197
|
-
wrote: Object.keys(updates),
|
|
198
|
-
env_path: envPath,
|
|
199
|
-
ingest_url_host: parsed.host,
|
|
200
|
-
server,
|
|
201
|
-
next: server.token_valid
|
|
202
|
-
? "毎朝の自動配信は crontab に `npx -y @clubs.place/cli@latest tick` を登録(clubs-nippo init のヘルプ参照)"
|
|
203
|
-
: "トークンがサーバで確認できませんでした。接続画面で再発行して register し直してください",
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (name === "clubs_link_status") {
|
|
208
|
-
const ingestUrl = process.env.NIPPO_INGEST_URL ?? "";
|
|
209
|
-
const ingestToken = process.env.NIPPO_INGEST_TOKEN ?? "";
|
|
210
|
-
const configured = Boolean(ingestUrl && ingestToken);
|
|
211
|
-
const server = configured
|
|
212
|
-
? await serverCheck(ingestUrl, ingestToken)
|
|
213
|
-
: { reachable: false, token_valid: false };
|
|
214
|
-
const history = readHistory();
|
|
215
|
-
return {
|
|
216
|
-
ok: configured && server.token_valid,
|
|
217
|
-
configured,
|
|
218
|
-
ingest_url_host: ingestUrl ? new URL(ingestUrl).host : null,
|
|
219
|
-
agent_name: process.env.AGENT_NAME ?? null,
|
|
220
|
-
server,
|
|
221
|
-
last_local_report_date: history.at(-1)?.date ?? null,
|
|
222
|
-
env_path: join(APP_HOME, ".env"),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return { ok: false, error: `unknown tool: ${name}` };
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
// ── JSON-RPC 2.0 / stdio ─────────────────────────────────
|
|
230
|
-
|
|
231
|
-
const reply = (id, result) => {
|
|
232
|
-
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const replyError = (id, code, message) => {
|
|
236
|
-
process.stdout.write(
|
|
237
|
-
JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n",
|
|
238
|
-
);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
export const runMcpServer = async () => {
|
|
242
|
-
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
243
|
-
console.error(`[clubs-nippo mcp] ${DEPRECATION}`);
|
|
244
|
-
console.error(
|
|
245
|
-
`[clubs-nippo mcp] stdio server ready (tools: ${TOOLS.map((t) => t.name).join(", ")})`,
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
for await (const line of rl) {
|
|
249
|
-
if (!line.trim()) continue;
|
|
250
|
-
let msg;
|
|
251
|
-
try {
|
|
252
|
-
msg = JSON.parse(line);
|
|
253
|
-
} catch {
|
|
254
|
-
continue; // 不正な行は無視(プロトコル外のノイズ)
|
|
255
|
-
}
|
|
256
|
-
const { id, method, params } = msg;
|
|
257
|
-
|
|
258
|
-
// 通知(id なし)は initialized 含め応答しない
|
|
259
|
-
if (id === undefined || id === null) continue;
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
if (method === "initialize") {
|
|
263
|
-
reply(id, {
|
|
264
|
-
protocolVersion: params?.protocolVersion ?? PROTOCOL_FALLBACK,
|
|
265
|
-
capabilities: { tools: { listChanged: false } },
|
|
266
|
-
serverInfo: { name: "clubs-nippo", version: pkgVersion() },
|
|
267
|
-
instructions: DEPRECATION,
|
|
268
|
-
});
|
|
269
|
-
} else if (method === "ping") {
|
|
270
|
-
reply(id, {});
|
|
271
|
-
} else if (method === "tools/list") {
|
|
272
|
-
reply(id, { tools: TOOLS });
|
|
273
|
-
} else if (method === "tools/call") {
|
|
274
|
-
const result = await callTool(params?.name, params?.arguments ?? {});
|
|
275
|
-
reply(id, {
|
|
276
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
277
|
-
isError: result.ok === false,
|
|
278
|
-
});
|
|
279
|
-
} else {
|
|
280
|
-
replyError(id, -32601, `method not found: ${method}`);
|
|
281
|
-
}
|
|
282
|
-
} catch (e) {
|
|
283
|
-
replyError(id, -32603, e?.message ?? "internal error");
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
};
|