@clubs.place/cli 0.1.23 → 0.1.25
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/PROVENANCE.json +9 -10
- package/README.md +13 -15
- package/bin/clubs.mjs +13 -11
- package/package.json +1 -1
- package/src/autorun.mjs +1 -1
- package/src/autorun.test.mjs +2 -2
- package/src/profiles.mjs +12 -1
- package/src/profiles.test.mjs +9 -2
- package/src/mcp.mjs +0 -264
package/PROVENANCE.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@clubs.place/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"repository": "https://github.com/frame00/@clubs.place/cli",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "ff15dd9dd15a9cb332288d6524e74d8de47614c7",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"generated_at": "2026-10-
|
|
7
|
+
"generated_at": "2026-10-28T13:13:07.338Z",
|
|
8
8
|
"files": {
|
|
9
9
|
".env.example": "sha256-6b151ffb18a84ae9e468d77e82d6af1ebb344ee5f4a45f065e396c56859d6cf6",
|
|
10
|
-
"PROVENANCE.json": "sha256-
|
|
10
|
+
"PROVENANCE.json": "sha256-82a47fe5806c0f66050a9b9d8cba39135311988e25b2e0c5c4e56b1252f3778d",
|
|
11
11
|
"bin/clubs-nippo": "sha256-03868da2fca1cbd333f2260d08bcb0b14a258a7e7df0da8bfce3e2ed7bee2c62",
|
|
12
12
|
"bin/clubs-nippo-web.mjs": "sha256-23734c47f22cd686c30fd3425b904174ee265ca113a66c34b45916905ff06b4a",
|
|
13
13
|
"bin/clubs-nippo.mjs": "sha256-51102bf7467c7794770c6046305384f2209c450136af3dced39b6cc2a974cff7",
|
|
14
|
-
"bin/clubs.mjs": "sha256-
|
|
14
|
+
"bin/clubs.mjs": "sha256-2f50a079abfe134b81446e5ade7157e3c4b800b22475d6842f35e1d93a7d02d4",
|
|
15
15
|
"docs/events-schema.md": "sha256-6be7a614351c2a3dbf15c5201dbf5d466649e4b91d17206c603f0c26b0290256",
|
|
16
16
|
"examples/hermes-events.sample.jsonl": "sha256-bb895a3d5abeeab10a46835af2771d077b71564286a816caf8d73682dbfec9cf",
|
|
17
17
|
"src/aggregate.mjs": "sha256-2b8c9185ee133a115e98e2ed6d05c536e4d9f6cc71e390ceab2fca89f7dc6512",
|
|
18
18
|
"src/aggregate.slots.test.mjs": "sha256-e0952c2cd41ca974a933d37a7737c3805620bff3398f3789bc4fcffa8bed5049",
|
|
19
|
-
"src/autorun.mjs": "sha256-
|
|
20
|
-
"src/autorun.test.mjs": "sha256-
|
|
19
|
+
"src/autorun.mjs": "sha256-0c8de9555458536b4070185d8590e7a40f6f0d88b89f1312ebdbea6f1c064446",
|
|
20
|
+
"src/autorun.test.mjs": "sha256-868260443886a4ee29ce259f890f04615d0ce69d651ff9960441a5255f2358cd",
|
|
21
21
|
"src/cards.mjs": "sha256-e301d3039eccbab6efad4a98679de898be75f95dc3542aaab15a1d629191eca5",
|
|
22
22
|
"src/collect.mjs": "sha256-60f9d89d373fd8392008aaf6055815248e76485a61d72afcc6aa0b2b6d84a343",
|
|
23
23
|
"src/config.mjs": "sha256-aec9aed44e618d5c1adc6ed9b314de23aefcc9c0f7c26f827f461d127143a0ea",
|
|
@@ -33,11 +33,10 @@
|
|
|
33
33
|
"src/lemma.docHash.test.mjs": "sha256-7446ac23bdfbe7e92490134cbe74470266afc1efd176c3e40078d70bd1d1a141",
|
|
34
34
|
"src/lemma.mjs": "sha256-48fdf89267bf09c90eafde180f0fe00fb443ea0dbf87b7dd970689d1fc192675",
|
|
35
35
|
"src/mask.mjs": "sha256-324c47cf78db4596965f0e0f39ff8f63ed794baf8b9f1796de5feb0e69891471",
|
|
36
|
-
"src/mcp.mjs": "sha256-bc1ee0e2029c805aec0911d995f5de72bf2ceece52ce70b452fd1c28d308f490",
|
|
37
36
|
"src/narrative.blockRepos.test.mjs": "sha256-0be41c34bf5a2848667dbca9456cd7eac250f19deaec5bd2ba36ae572baf7ad3",
|
|
38
37
|
"src/narrative.mjs": "sha256-5b29d30c451fd05781384c4793623c303c4e52333cb70f3ef52a4164094f5ccb",
|
|
39
|
-
"src/profiles.mjs": "sha256-
|
|
40
|
-
"src/profiles.test.mjs": "sha256-
|
|
38
|
+
"src/profiles.mjs": "sha256-b5f3fe14d2469334e199a2e4a4a870735940f66117ec80c969ff300aae565c69",
|
|
39
|
+
"src/profiles.test.mjs": "sha256-c781597ba9d1da643a41e732ff8fe1669972d39ab2a60b8ce6b310aabde2fcf5",
|
|
41
40
|
"src/prompt.mjs": "sha256-0f0f01bc4b8b88cd752dd667f6b903847fc0e9f09d5e98a7c05f3d3873085f98",
|
|
42
41
|
"src/prompt.test.mjs": "sha256-972c6962fd7baea0495a3f17694944a841c9b2f90c1f7397264c5e4aca8b2d48",
|
|
43
42
|
"src/remote-config.mjs": "sha256-8988c6dac675775e6e33fd3aeda71d335eb2880bcfe9b5266f78e8a023fe74d6",
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# clubs-nippo — AI業務日報
|
|
2
2
|
|
|
3
3
|
AIエージェントの実行ログから、前日ぶんの日報を自動生成してメール送信する CLI ツール。
|
|
4
|
+
CLUBS の日報の自動化専用(AI を CLUBS につなぐのは接続画面のリモート MCP)。
|
|
4
5
|
|
|
5
6
|
- **pull 型**: 生ログはあなたの端末を出ない。集計値とマスク済み本文だけを送る
|
|
6
7
|
- **依存ゼロ**: Node 20+ の標準機能のみ。`npm install` 不要
|
|
@@ -11,7 +12,7 @@ AIエージェントの実行ログから、前日ぶんの日報を自動生成
|
|
|
11
12
|
CLUBS の接続画面が発行するコマンドをそのまま貼る:
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
|
-
npx clubs
|
|
15
|
+
npx @clubs.place/cli@latest init --url=<取込URL> --code=<接続コード>
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
接続コードは 10 分・1 回限り有効で、init がその場で連携トークンに引き換える
|
|
@@ -27,9 +28,9 @@ AI が一度も動かなかった日は日報にならない(メールも来
|
|
|
27
28
|
## 使い方
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
|
-
npx clubs
|
|
31
|
-
npx clubs
|
|
32
|
-
npx clubs
|
|
31
|
+
npx @clubs.place/cli@latest # 前日ぶんを生成して送信
|
|
32
|
+
npx @clubs.place/cli@latest --dry-run # 送信せず out/ に HTML を書くだけ
|
|
33
|
+
npx @clubs.place/cli@latest --date=2026-07-13 # 対象日を指定
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
### 1台で複数の AI(Claude Code と Codex など)
|
|
@@ -40,21 +41,18 @@ npx clubs-nippo@latest --date=2026-07-13 # 対象日を指定
|
|
|
40
41
|
ときは、これまでどおり上書きします。
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
|
-
npx clubs
|
|
44
|
+
npx @clubs.place/cli@latest --date=2026-10-08 --only=claude-code # その収集元だけ(あとから作るとき)
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
##
|
|
47
|
+
## AI を CLUBS につなぐ
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
この CLI は**日報の自動化だけ**を受け持つ。AI を CLUBS につなぐ(案件・ルールを
|
|
50
|
+
読む・残す)のは、CLUBS の接続画面(https://clubs.place/connections )が案内する
|
|
51
|
+
リモート MCP(URL+許可)で行う。
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
ツールは `clubs_register`(接続画面が発行した取込URL・連携トークンで登録)と
|
|
56
|
-
`clubs_link_status`(設定・サーバ疎通・トークン有効性)の2本だけ。日報の
|
|
57
|
-
取り込み・シールの発行は MCP からはできない(判定・発行はサーバ専権)。
|
|
53
|
+
以前の `claude mcp add clubs -- npx … mcp`(手元の MCP・`clubs_register` /
|
|
54
|
+
`clubs_link_status`)は 0.1.25 で終わった。登録してあれば `claude mcp remove clubs` で外し、
|
|
55
|
+
接続画面からつなぎ直す。
|
|
58
56
|
|
|
59
57
|
## 環境変数
|
|
60
58
|
|
package/bin/clubs.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
listProfiles,
|
|
12
12
|
readEnvFile,
|
|
13
13
|
runProfiles,
|
|
14
|
+
writeLastInit,
|
|
14
15
|
} from "../src/profiles.mjs";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { collect } from "../src/collect.mjs";
|
|
@@ -132,11 +133,6 @@ const HELP = `clubs-nippo — Claude Code の実行ログから前日ぶんの A
|
|
|
132
133
|
clubs-nippo cards カードを評価して発行(送信はしない)
|
|
133
134
|
clubs-nippo cards --dry-run 評価するが発行しない。しきい値を試すときは必ずこちら
|
|
134
135
|
(達成カードは剥奪できないので、誤発行は取り消せない)
|
|
135
|
-
clubs-nippo mcp MCP サーバー(stdio)として起動。AIクライアント
|
|
136
|
-
(Claude Code / Cursor 等)に登録すると、対話の中で
|
|
137
|
-
CLUBS への登録(clubs_register)と疎通確認
|
|
138
|
-
(clubs_link_status)ができる。例:
|
|
139
|
-
claude mcp add clubs -- npx -y clubs-nippo@latest mcp
|
|
140
136
|
|
|
141
137
|
収集元:
|
|
142
138
|
--source=claude-code この端末の Claude Code セッション記録(既定)
|
|
@@ -171,11 +167,16 @@ const main = async () => {
|
|
|
171
167
|
return;
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
// ── mcp: MCP
|
|
170
|
+
// ── mcp: 手元の MCP は終わった(CLUBS 判断 #26・2026-10-16)──────────
|
|
171
|
+
// 前に登録した人の AI がこれを起動したとき、黙って落ちないように理由を出す。
|
|
172
|
+
// stdout は MCP のプロトコルが読むので、知らせは stderr に出す。
|
|
175
173
|
if (argv._[0] === "mcp") {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
console.error(
|
|
175
|
+
"[clubs mcp] 手元の MCP(clubs_register / clubs_link_status)は終わりました。" +
|
|
176
|
+
"CLUBS の接続画面 https://clubs.place/connections からリモート MCP につなぎ直し、" +
|
|
177
|
+
"この登録は `claude mcp remove clubs` などで外してください。日報の自動化(tick)はこのまま動きます。",
|
|
178
|
+
);
|
|
179
|
+
process.exit(1);
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
// ── init: 接続画面が発行するコマンド1行で .env を書く(npx 導入の入口)──
|
|
@@ -285,6 +286,7 @@ const main = async () => {
|
|
|
285
286
|
else lines.push(row);
|
|
286
287
|
}
|
|
287
288
|
writeFileSync(envPath, lines.join("\n") + "\n", "utf8");
|
|
289
|
+
writeLastInit(APP_HOME, envPath);
|
|
288
290
|
console.log(`設定を書きました: ${envPath}`);
|
|
289
291
|
console.log(` ${Object.keys(updates).join(", ")}`);
|
|
290
292
|
// cron は PATH が細く npx が見つからないことが多い。実際に動いている node の
|
|
@@ -295,7 +297,7 @@ const main = async () => {
|
|
|
295
297
|
if (home !== APP_HOME) {
|
|
296
298
|
// 自動実行は1本のまま。毎時の tick が並べた AI の分も動かす。
|
|
297
299
|
console.log("毎朝の自動実行は今のものがこの AI の分も動かします(登録し直しは不要)。");
|
|
298
|
-
console.log(`今すぐ試す(前日分を発行): CLUBS_NIPPO_HOME=${home} npx clubs
|
|
300
|
+
console.log(`今すぐ試す(前日分を発行): CLUBS_NIPPO_HOME=${home} npx @clubs.place/cli@latest`);
|
|
299
301
|
return;
|
|
300
302
|
}
|
|
301
303
|
const yes = await promptYesNo("毎朝の自動実行を登録しますか? [Y/n]");
|
|
@@ -312,7 +314,7 @@ const main = async () => {
|
|
|
312
314
|
console.log(` ${hourlyLine}`);
|
|
313
315
|
}
|
|
314
316
|
console.log("");
|
|
315
|
-
console.log("今すぐ試す(前日分を発行): npx clubs
|
|
317
|
+
console.log("今すぐ試す(前日分を発行): npx @clubs.place/cli@latest");
|
|
316
318
|
return;
|
|
317
319
|
}
|
|
318
320
|
|
package/package.json
CHANGED
package/src/autorun.mjs
CHANGED
package/src/autorun.test.mjs
CHANGED
|
@@ -16,14 +16,14 @@ test("macOS では caffeinate で包む(実行中のスリープを止める
|
|
|
16
16
|
"--",
|
|
17
17
|
NPX,
|
|
18
18
|
"-y",
|
|
19
|
-
"clubs
|
|
19
|
+
"@clubs.place/cli@latest",
|
|
20
20
|
"tick",
|
|
21
21
|
]);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test("macOS 以外では包まない(caffeinate が無い)", () => {
|
|
25
25
|
for (const plat of ["linux", "freebsd", "win32"]) {
|
|
26
|
-
assert.deepEqual(tickCommand(NPX, plat), [NPX, "-y", "clubs
|
|
26
|
+
assert.deepEqual(tickCommand(NPX, plat), [NPX, "-y", "@clubs.place/cli@latest", "tick"]);
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
package/src/profiles.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
|
|
@@ -18,6 +18,17 @@ import { spawn } from "node:child_process";
|
|
|
18
18
|
|
|
19
19
|
export const PROFILE_DIR = "agents";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 最後の init が書いた .env の場所(APP_HOME/.last-init に1行)。
|
|
23
|
+
* インストールのスクリプトは MCP の Bearer をここから読む——2つ目の AI は
|
|
24
|
+
* agents/<ID>/.env に書かれるので、APP_HOME の .env を読むと前の AI の鍵で
|
|
25
|
+
* 登録して 401 になる(2026-10-24)。
|
|
26
|
+
*/
|
|
27
|
+
export const LAST_INIT_FILE = ".last-init";
|
|
28
|
+
|
|
29
|
+
export const writeLastInit = (home, envPath) =>
|
|
30
|
+
writeFileSync(join(home, LAST_INIT_FILE), envPath + "\n", "utf8");
|
|
31
|
+
|
|
21
32
|
/** 子プロセスの目印(子は並べた分をさらに回さない)。 */
|
|
22
33
|
export const CHILD_FLAG = "CLUBS_NIPPO_CHILD";
|
|
23
34
|
|
package/src/profiles.test.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { chooseInitHome, childEnv, listProfiles, CHILD_FLAG } from "./profiles.mjs";
|
|
6
|
+
import { chooseInitHome, childEnv, listProfiles, writeLastInit, CHILD_FLAG, LAST_INIT_FILE } from "./profiles.mjs";
|
|
7
7
|
|
|
8
8
|
const home = () => mkdtempSync(join(tmpdir(), "nippo-"));
|
|
9
9
|
|
|
@@ -64,3 +64,10 @@ test("子プロセスには APP_HOME の .env の値を渡さない", () => {
|
|
|
64
64
|
assert.equal(env.CLUBS_NIPPO_HOME, "/x");
|
|
65
65
|
assert.equal(env[CHILD_FLAG], "1");
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
test("最後に書いた .env の場所を APP_HOME/.last-init に残す(並べた AI でも)", () => {
|
|
69
|
+
const h = home();
|
|
70
|
+
const env = join(h, "agents", "B", ".env");
|
|
71
|
+
writeLastInit(h, env);
|
|
72
|
+
assert.equal(readFileSync(join(h, LAST_INIT_FILE), "utf8"), env + "\n");
|
|
73
|
+
});
|
package/src/mcp.mjs
DELETED
|
@@ -1,264 +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
|
-
* 依存ゼロ方針(このリポの流儀)のため SDK は使わず、MCP の stdio
|
|
15
|
-
* トランスポート(JSON-RPC 2.0・1行1メッセージ)を最小実装する。
|
|
16
|
-
* stdout はプロトコル専用。ログは必ず stderr へ。
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { createInterface } from "node:readline";
|
|
20
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
import { APP_HOME, ROOT } from "./config.mjs";
|
|
23
|
-
import { collectorConfigUrl } from "./remote-config.mjs";
|
|
24
|
-
import { readHistory } from "./history.mjs";
|
|
25
|
-
|
|
26
|
-
const PROTOCOL_FALLBACK = "2025-06-18";
|
|
27
|
-
|
|
28
|
-
const pkgVersion = () => {
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
|
|
31
|
-
} catch {
|
|
32
|
-
return "0.0.0";
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// ── 登録先 URL のガード(注入面対策) ─────────────────────
|
|
37
|
-
// MCP 経由の register は AI の文脈から呼ばれるため、細工された取込URLで
|
|
38
|
-
// トークンや日報の送り先を挿げ替えられないよう、既定で CLUBS のホストに限る。
|
|
39
|
-
// 自前ホストの開発者は CLUBS_NIPPO_ALLOW_HOST=<host> で1件だけ追加できる。
|
|
40
|
-
const urlAllowed = (raw) => {
|
|
41
|
-
try {
|
|
42
|
-
const u = new URL(raw);
|
|
43
|
-
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
44
|
-
if (local) return true; // ローカル開発
|
|
45
|
-
if (u.protocol !== "https:") return false;
|
|
46
|
-
if (u.hostname === "clubs.place" || u.hostname.endsWith(".clubs.place"))
|
|
47
|
-
return true;
|
|
48
|
-
return u.hostname === process.env.CLUBS_NIPPO_ALLOW_HOST;
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/** init コマンドと同じ書式で .env を更新(既存キーは生かす)。 */
|
|
55
|
-
const writeEnvUpdates = (updates) => {
|
|
56
|
-
mkdirSync(join(APP_HOME, "out"), { recursive: true });
|
|
57
|
-
const envPath = join(APP_HOME, ".env");
|
|
58
|
-
const lines = existsSync(envPath)
|
|
59
|
-
? readFileSync(envPath, "utf8").replace(/\n$/, "").split("\n")
|
|
60
|
-
: [
|
|
61
|
-
"# clubs-nippo の設定。キーの一覧は https://www.npmjs.com/package/clubs-nippo を参照。",
|
|
62
|
-
];
|
|
63
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
64
|
-
const row = `${key}=${value}`;
|
|
65
|
-
const idx = lines.findIndex((l) => l.match(new RegExp(`^\\s*${key}\\s*=`)));
|
|
66
|
-
if (idx >= 0) lines[idx] = row;
|
|
67
|
-
else lines.push(row);
|
|
68
|
-
}
|
|
69
|
-
writeFileSync(envPath, lines.join("\n") + "\n", "utf8");
|
|
70
|
-
return envPath;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const withTimeout = async (fn, ms) => {
|
|
74
|
-
const ctrl = new AbortController();
|
|
75
|
-
const timer = setTimeout(() => ctrl.abort(), ms);
|
|
76
|
-
try {
|
|
77
|
-
return await fn(ctrl.signal);
|
|
78
|
-
} finally {
|
|
79
|
-
clearTimeout(timer);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/** 連携トークンの実在確認=collector-config にサーバ照会する。 */
|
|
84
|
-
const serverCheck = async (ingestUrl, ingestToken) => {
|
|
85
|
-
const url = collectorConfigUrl(ingestUrl);
|
|
86
|
-
if (!url || !ingestToken) return { reachable: false, token_valid: false };
|
|
87
|
-
try {
|
|
88
|
-
const res = await withTimeout(
|
|
89
|
-
(signal) =>
|
|
90
|
-
fetch(url, {
|
|
91
|
-
signal,
|
|
92
|
-
headers: { Authorization: `Bearer ${ingestToken}` },
|
|
93
|
-
}),
|
|
94
|
-
10_000,
|
|
95
|
-
);
|
|
96
|
-
return { reachable: true, token_valid: res.ok };
|
|
97
|
-
} catch {
|
|
98
|
-
return { reachable: false, token_valid: false };
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// ── ツール定義 ────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
const TOOLS = [
|
|
105
|
-
{
|
|
106
|
-
name: "clubs_register",
|
|
107
|
-
description:
|
|
108
|
-
"この端末を CLUBS に登録する(clubs-nippo init と同じ設定を書く)。url と token は CLUBS の接続画面(/nippo/connections)が発行した値をそのまま渡すこと。登録後にサーバへ疎通確認する。",
|
|
109
|
-
inputSchema: {
|
|
110
|
-
type: "object",
|
|
111
|
-
properties: {
|
|
112
|
-
url: {
|
|
113
|
-
type: "string",
|
|
114
|
-
description: "取込URL(接続画面が発行。…/api/nippo/ingest)",
|
|
115
|
-
},
|
|
116
|
-
token: { type: "string", description: "連携トークン(接続画面が発行)" },
|
|
117
|
-
source: {
|
|
118
|
-
type: "string",
|
|
119
|
-
description: "収集元(例: claude-code / cowork。省略可)",
|
|
120
|
-
},
|
|
121
|
-
agent_name: { type: "string", description: "体の表示名(省略可)" },
|
|
122
|
-
owner_name: { type: "string", description: "オーナー名(省略可)" },
|
|
123
|
-
},
|
|
124
|
-
required: ["url", "token"],
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: "clubs_link_status",
|
|
129
|
-
description:
|
|
130
|
-
"CLUBS との連携状態を返す(設定の有無・サーバ疎通・トークン有効性・ローカル最終記録日)。",
|
|
131
|
-
inputSchema: { type: "object", properties: {} },
|
|
132
|
-
},
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
const callTool = async (name, args = {}) => {
|
|
136
|
-
if (name === "clubs_register") {
|
|
137
|
-
const url = String(args.url ?? "");
|
|
138
|
-
const token = String(args.token ?? "");
|
|
139
|
-
if (!url || !token) {
|
|
140
|
-
return { ok: false, error: "url と token は必須です" };
|
|
141
|
-
}
|
|
142
|
-
let parsed;
|
|
143
|
-
try {
|
|
144
|
-
parsed = new URL(url);
|
|
145
|
-
} catch {
|
|
146
|
-
return { ok: false, error: "url が不正です" };
|
|
147
|
-
}
|
|
148
|
-
if (!/\/api\/nippo\/ingest\/?$/.test(parsed.pathname)) {
|
|
149
|
-
return {
|
|
150
|
-
ok: false,
|
|
151
|
-
error: "url は接続画面が発行した取込URL(…/api/nippo/ingest)を渡してください",
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (!urlAllowed(url)) {
|
|
155
|
-
return {
|
|
156
|
-
ok: false,
|
|
157
|
-
error:
|
|
158
|
-
"この url のホストには登録できません(clubs.place / localhost のみ。自前ホストは CLUBS_NIPPO_ALLOW_HOST で許可)",
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
const updates = Object.fromEntries(
|
|
162
|
-
Object.entries({
|
|
163
|
-
NIPPO_INGEST_URL: url,
|
|
164
|
-
NIPPO_INGEST_TOKEN: token,
|
|
165
|
-
SOURCE: args.source,
|
|
166
|
-
AGENT_NAME: args.agent_name,
|
|
167
|
-
OWNER_NAME: args.owner_name,
|
|
168
|
-
}).filter(([, v]) => v),
|
|
169
|
-
);
|
|
170
|
-
const envPath = writeEnvUpdates(updates);
|
|
171
|
-
// 同一セッション内の link_status が新しい値を見られるように反映する
|
|
172
|
-
// (config.mjs の loadDotEnv は起動時に未設定キーしか埋めない)
|
|
173
|
-
for (const [key, value] of Object.entries(updates)) process.env[key] = value;
|
|
174
|
-
const server = await serverCheck(url, token);
|
|
175
|
-
return {
|
|
176
|
-
ok: server.token_valid,
|
|
177
|
-
wrote: Object.keys(updates),
|
|
178
|
-
env_path: envPath,
|
|
179
|
-
ingest_url_host: parsed.host,
|
|
180
|
-
server,
|
|
181
|
-
next: server.token_valid
|
|
182
|
-
? "毎朝の自動配信は crontab に `npx -y clubs-nippo@latest tick` を登録(clubs-nippo init のヘルプ参照)"
|
|
183
|
-
: "トークンがサーバで確認できませんでした。接続画面で再発行して register し直してください",
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (name === "clubs_link_status") {
|
|
188
|
-
const ingestUrl = process.env.NIPPO_INGEST_URL ?? "";
|
|
189
|
-
const ingestToken = process.env.NIPPO_INGEST_TOKEN ?? "";
|
|
190
|
-
const configured = Boolean(ingestUrl && ingestToken);
|
|
191
|
-
const server = configured
|
|
192
|
-
? await serverCheck(ingestUrl, ingestToken)
|
|
193
|
-
: { reachable: false, token_valid: false };
|
|
194
|
-
const history = readHistory();
|
|
195
|
-
return {
|
|
196
|
-
ok: configured && server.token_valid,
|
|
197
|
-
configured,
|
|
198
|
-
ingest_url_host: ingestUrl ? new URL(ingestUrl).host : null,
|
|
199
|
-
agent_name: process.env.AGENT_NAME ?? null,
|
|
200
|
-
server,
|
|
201
|
-
last_local_report_date: history.at(-1)?.date ?? null,
|
|
202
|
-
env_path: join(APP_HOME, ".env"),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return { ok: false, error: `unknown tool: ${name}` };
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
// ── JSON-RPC 2.0 / stdio ─────────────────────────────────
|
|
210
|
-
|
|
211
|
-
const reply = (id, result) => {
|
|
212
|
-
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const replyError = (id, code, message) => {
|
|
216
|
-
process.stdout.write(
|
|
217
|
-
JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n",
|
|
218
|
-
);
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
export const runMcpServer = async () => {
|
|
222
|
-
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
223
|
-
console.error(
|
|
224
|
-
`[clubs-nippo mcp] stdio server ready (tools: ${TOOLS.map((t) => t.name).join(", ")})`,
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
for await (const line of rl) {
|
|
228
|
-
if (!line.trim()) continue;
|
|
229
|
-
let msg;
|
|
230
|
-
try {
|
|
231
|
-
msg = JSON.parse(line);
|
|
232
|
-
} catch {
|
|
233
|
-
continue; // 不正な行は無視(プロトコル外のノイズ)
|
|
234
|
-
}
|
|
235
|
-
const { id, method, params } = msg;
|
|
236
|
-
|
|
237
|
-
// 通知(id なし)は initialized 含め応答しない
|
|
238
|
-
if (id === undefined || id === null) continue;
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
if (method === "initialize") {
|
|
242
|
-
reply(id, {
|
|
243
|
-
protocolVersion: params?.protocolVersion ?? PROTOCOL_FALLBACK,
|
|
244
|
-
capabilities: { tools: { listChanged: false } },
|
|
245
|
-
serverInfo: { name: "clubs-nippo", version: pkgVersion() },
|
|
246
|
-
});
|
|
247
|
-
} else if (method === "ping") {
|
|
248
|
-
reply(id, {});
|
|
249
|
-
} else if (method === "tools/list") {
|
|
250
|
-
reply(id, { tools: TOOLS });
|
|
251
|
-
} else if (method === "tools/call") {
|
|
252
|
-
const result = await callTool(params?.name, params?.arguments ?? {});
|
|
253
|
-
reply(id, {
|
|
254
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
255
|
-
isError: result.ok === false,
|
|
256
|
-
});
|
|
257
|
-
} else {
|
|
258
|
-
replyError(id, -32601, `method not found: ${method}`);
|
|
259
|
-
}
|
|
260
|
-
} catch (e) {
|
|
261
|
-
replyError(id, -32603, e?.message ?? "internal error");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
};
|