@clubs.place/cli 0.0.0-stage → 0.1.17

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 (70) hide show
  1. package/.env.example +88 -0
  2. package/LICENSE +21 -0
  3. package/PROVENANCE.json +77 -0
  4. package/README.md +70 -2
  5. package/bin/clubs-nippo +7 -0
  6. package/bin/clubs-nippo-web.mjs +338 -0
  7. package/bin/clubs-nippo.mjs +527 -0
  8. package/docs/events-schema.md +76 -0
  9. package/examples/hermes-events.sample.jsonl +8 -0
  10. package/package.json +48 -4
  11. package/src/aggregate.mjs +105 -0
  12. package/src/autorun.mjs +25 -0
  13. package/src/autorun.test.mjs +43 -0
  14. package/src/cards.mjs +406 -0
  15. package/src/collect.mjs +111 -0
  16. package/src/config.mjs +173 -0
  17. package/src/generators/claude.mjs +31 -0
  18. package/src/generators/cli.mjs +81 -0
  19. package/src/generators/codex.mjs +28 -0
  20. package/src/generators/copilot.mjs +21 -0
  21. package/src/generators/gemini.mjs +20 -0
  22. package/src/generators/openai-compat.mjs +79 -0
  23. package/src/generators/openrouter.mjs +65 -0
  24. package/src/history.mjs +66 -0
  25. package/src/ingest.mjs +78 -0
  26. package/src/lemma.docHash.test.mjs +37 -0
  27. package/src/lemma.mjs +303 -0
  28. package/src/mask.mjs +45 -0
  29. package/src/mcp.mjs +269 -0
  30. package/src/narrative.mjs +191 -0
  31. package/src/prompt.mjs +184 -0
  32. package/src/prompt.test.mjs +146 -0
  33. package/src/remote-config.mjs +72 -0
  34. package/src/render.mjs +467 -0
  35. package/src/send.mjs +36 -0
  36. package/src/settings.mjs +54 -0
  37. package/src/sources/claude-code.mjs +174 -0
  38. package/src/sources/codex-cli.mjs +182 -0
  39. package/src/sources/copilot-cli.mjs +125 -0
  40. package/src/sources/cowork.mjs +193 -0
  41. package/src/sources/events-file.mjs +120 -0
  42. package/src/sources/gemini-cli.mjs +128 -0
  43. package/src/sources/hermes.mjs +427 -0
  44. package/src/store.mjs +227 -0
  45. package/src/tasks.mjs +229 -0
  46. package/src/tool-name.mjs +28 -0
  47. package/templates/report-v3.reference.html +162 -0
  48. package/web/assets/logo-clubs-white.png +0 -0
  49. package/web/assets/logo-clubs-white.svg +7 -0
  50. package/web/assets/logo-clubs-white.svg.tmp +30 -0
  51. package/web/assets/logo-clubs.svg +30 -0
  52. package/web/assets/qr.js +411 -0
  53. package/web/assets/sidebar.js +154 -0
  54. package/web/assets/twemoji.js +34 -0
  55. package/web/cards/achievements.html +230 -0
  56. package/web/cards/catalog.json +217 -0
  57. package/web/email/hero-v3.reference.html +123 -0
  58. package/web/home/index.html +269 -0
  59. package/web/preview.json +301 -0
  60. package/web/qr-selftest.html +46 -0
  61. package/web/reports/edit.html +255 -0
  62. package/web/reports/edit.reference.html +257 -0
  63. package/web/reports/index.html +196 -0
  64. package/web/settings/index.html +219 -0
  65. package/web/share/cards-reference.html +228 -0
  66. package/web/share/passport.html +214 -0
  67. package/web/share/teianbako.html +177 -0
  68. package/web/signup/index.html +326 -0
  69. package/web/tasks/index.html +229 -0
  70. package/web/verify/index.html +248 -0
package/.env.example ADDED
@@ -0,0 +1,88 @@
1
+ # cp .env.example .env して埋める。.env は .gitignore 済み(コミットされない)。
2
+
3
+ # ── メール送信(Phase 1・必須) ──────────────────────────────
4
+ EMAIL_PROVIDER=resend
5
+ # Resend の API キー(https://resend.com/api-keys)
6
+ EMAIL_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxx
7
+ # 送信元。独自ドメインを検証していない間は onboarding@resend.dev が使える
8
+ # (その場合、宛先は Resend に登録した自分のアドレスのみ)
9
+ EMAIL_FROM=CLUBS 日報 <onboarding@resend.dev>
10
+ # 宛先。カンマ区切りで複数可
11
+ EMAIL_TO=mayumi@frame00.com
12
+
13
+ # ── 収集元(エージェントごとに1つ) ──────────────────────────
14
+ # claude-code = この端末の Claude Code セッション記録(既定)。名義は必ず Claude
15
+ # events-file = エージェントが書き出した JSONL(Hermes など)。docs/events-schema.md
16
+ SOURCE=claude-code,cowork
17
+ # EVENTS_FILE=/path/to/hermes-events.jsonl
18
+
19
+ # ── 日報の中身 ───────────────────────────────────────────
20
+ # 対象日。未設定なら「前日(JST)」
21
+ # REPORT_DATE=2026-07-13
22
+ # AGENT_NAME が効くのは SOURCE=events-file のときだけ。
23
+ # claude-code では収集元が名義を決めるため無視される(帰属の規律)
24
+ # AGENT_NAME=Hermes
25
+ # MODEL_LABEL=claude-code
26
+ SEAT_LABEL=席: FRAME00
27
+
28
+ # 顧客名などを伏せる正規表現。カンマ区切り。マッチ箇所は黒塗り+「マスク済み N箇所」表示になる
29
+ # MASK_PATTERNS=株式会社[ぁ-んァ-ヶ一-龠A-Za-z0-9]+,Acme Corp,西川町
30
+
31
+ # 実働推定でアイドルと見なす間隔(分)
32
+ IDLE_GAP_MIN=15
33
+ # 1 ならオーナーの指示文の先頭200字(マスク済)を本文生成の材料に含める。0 で完全に外す
34
+ INCLUDE_PROMPTS=1
35
+ # タイムゾーン(分)。540 = JST
36
+ TZ_OFFSET_MIN=540
37
+
38
+ # ── メールのリンクとロゴ ─────────────────────────────────
39
+ # 未設定なら該当のボタン/画像を出さない(存在しないページや壊れた画像を出さない)。
40
+ # CONSOLE_BASE=https://clubs.place # 「編集して記録する」の飛び先
41
+ # PASSPORT_BASE=https://clubs.place # 「パスポートを見る」の飛び先
42
+ # EMAIL_LOGO_URL=https://clubs.place/assets/logo-clubs-white.png
43
+ # Gmail は SVG を描画しないので PNG。scripts/gen-email-logo.mjs で書き出す。
44
+
45
+ # ── 本文生成 ─────────────────────────────────────────────
46
+ # claude(既定)= claude -p / openrouter = OpenRouter API /
47
+ # openai-compat = OpenAI互換カスタム(Sakana Fugu 等)/ none = LLM を使わない
48
+ GENERATOR=claude
49
+ # claude/codex 等の CLI が PATH に無い環境(cron/launchd)向け。フルパスで上書き。
50
+ # CLAUDE_BIN=/Users/you/.local/bin/claude
51
+ # 生成の上限時間(ミリ秒・既定 240000)。大きい日に timeout で fallback に落ちるなら伸ばす。
52
+ # GENERATOR_TIMEOUT_MS=240000
53
+ # OPENROUTER_API_KEY=sk-or-...
54
+ # OPENROUTER_MODEL=anthropic/claude-sonnet-4.5
55
+ # OpenAI互換カスタム(GENERATOR=openai-compat)。base URL/model は接続画面から
56
+ # 来てもよいが、API キーは常にこのローカル .env のみ(CLUBS には保存しない)。
57
+ # BACKEND_BASE_URL=https://api.example.com/v1
58
+ # BACKEND_API_KEY=sk-...
59
+ # BACKEND_MODEL=your-model-id
60
+
61
+ # ── Lemma 証明(Phase 2・無ければ自動で stub に落ちる) ────────
62
+ LEMMA_API_BASE=https://workers.lemma.workers.dev
63
+ # 未設定なら ✓ は付かず「照合予定」で完走する
64
+ # LEMMA_API_KEY=
65
+ # LEMMA_SCOPE_ID=
66
+ # 検証ページの base(例 https://clubs.place/verify)。設定するとメールにリンクが出る。
67
+ # Lemma の GET /v1/documents/:docHash が本番稼働するまでは空のままにすること。
68
+ # 今リンクを出すと踏んだ先が「未登録」になり、✓ を出して裏が取れないのと同じになる。
69
+ # LEMMA_VERIFY_BASE=
70
+
71
+ # ── 達成カードのしきい値(docs/card-catalog.md §4) ──────────
72
+ # ハードコードしない。チームごとに違う。
73
+ #
74
+ # 有効化してあるのは、カタログ §4 が「チームごとに違う」と明示した6キー。
75
+ # 変える前提なので、値が見えている状態にしておく。
76
+ # コメントアウトしてあるのは、カタログ本文に埋まっていた数値を設定キーに逃がしたもの。
77
+ # ほぼ変えないが、変えたくなったときに直せるよう口だけ開けてある。
78
+ BIGJOB_CALLS=40
79
+ NOFAIL_DAYS=30
80
+ PRECISION_DAYS=90
81
+ CLEAN_COUNT=10
82
+ TITLE_DAYS=10
83
+ MERGE_PASS_RATE=98
84
+ # 以下はカタログ本文の数値を設定キー化したもの(既定のままで使うことが多い)
85
+ # COMEBACK_EARLY_FAIL=30
86
+ # COMEBACK_FINAL_RATE=95
87
+ # ONE_TRICK_PCT=80
88
+ # OWL_AFTER=23:00
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FRAME00
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,77 @@
1
+ {
2
+ "package": "@clubs.place/cli",
3
+ "version": "0.1.17",
4
+ "repository": "https://github.com/frame00/clubs-nippo",
5
+ "commit": "ad877b6f664bb85cff66e70f667f2984f379b3de",
6
+ "license": "MIT",
7
+ "generated_at": "2026-10-16T12:27:44.831Z",
8
+ "files": {
9
+ ".env.example": "sha256-6b151ffb18a84ae9e468d77e82d6af1ebb344ee5f4a45f065e396c56859d6cf6",
10
+ "PROVENANCE.json": "sha256-a9f99617e3e6e5d7c8e69c4e06a6154bf58ca0364d3c95f9394ae3a4969427b2",
11
+ "bin/clubs-nippo": "sha256-0fea80ac0104be23e6750eb1f4db0970d95845511df5c83d35667feae2aaab06",
12
+ "bin/clubs-nippo-web.mjs": "sha256-8b8815655fce459eb49f3fd3cedc886549a8d8f19bb5fe3d57ac1476f18a7012",
13
+ "bin/clubs-nippo.mjs": "sha256-c84c2ea6117325c0525bc642febe98a27165598791528bc6293f4d630890d7f4",
14
+ "docs/events-schema.md": "sha256-b9491bb07217cf1391699105bb622b78f0340862c924719dbe294be0e2ffc359",
15
+ "examples/hermes-events.sample.jsonl": "sha256-bb895a3d5abeeab10a46835af2771d077b71564286a816caf8d73682dbfec9cf",
16
+ "src/aggregate.mjs": "sha256-bee812e1749b749a577cbcff5133a6ea0274bc9d2537e36a5f5c66ade2fc5c74",
17
+ "src/autorun.mjs": "sha256-c7f9455787d672d5fb6d2b0099d6b3410abc4c484b5cd31b7b3094841747cfea",
18
+ "src/autorun.test.mjs": "sha256-b2b6942840da6fae3bfc7a1b66aa61e78b3cd7a56bc174f5c6b919ed8d9d0637",
19
+ "src/cards.mjs": "sha256-0ab85c80bcfae55b60008c3d0f6ab7a384c7935672c07cb595c7f230e91dddcc",
20
+ "src/collect.mjs": "sha256-fbe2067d5d814f8b58e2ccc65107dd0eb5bac8d897e86d69f0f55a59d24d827d",
21
+ "src/config.mjs": "sha256-856e7f0fa59e0c45deec4e41c7f1b63c6fa311a7ad0f59fd184cbd8144a1553d",
22
+ "src/generators/claude.mjs": "sha256-ad38e35e5488686293607ec989057ec877af3bdc07405d281bf436b233265937",
23
+ "src/generators/cli.mjs": "sha256-52f84627e5dcbf4e92c81014f06fa74d44d8b671df59a1cde1391b65b0a29f3d",
24
+ "src/generators/codex.mjs": "sha256-d7c3595a74a8f166b0758ce6b88369b9541727478d39cbf717feb623f1d68abc",
25
+ "src/generators/copilot.mjs": "sha256-a3a802742b44d273f5f90bbda8f3a7bf5582064e4a3bdc19f0340e13cc63665e",
26
+ "src/generators/gemini.mjs": "sha256-9da520404135af756dd995ecc22b1696a5e78e916d5aa5763ce113ae35cc09b4",
27
+ "src/generators/openai-compat.mjs": "sha256-e08be0246f59f8d42bb9d4186f099389d0c907620a4da325c492890bd101cecc",
28
+ "src/generators/openrouter.mjs": "sha256-e3854d089b695110270bb4586e36afda19c3b00f92874cd68fd48dcc0999922f",
29
+ "src/history.mjs": "sha256-5862ca97515705f8b30b1df71884df6b39a21d645325a1501ab20e3ebd62ca87",
30
+ "src/ingest.mjs": "sha256-f9c5bf83ff3044bb74adf854c29dd3dfe075ec75076c422ab3d57f8d3e509a10",
31
+ "src/lemma.docHash.test.mjs": "sha256-974be9d96cb8da296bd438bcfe13649e6ec2d13117341629d471357f1fd46158",
32
+ "src/lemma.mjs": "sha256-e5c0dbfa9b8b7f2c6a4af04adb4cc7a13263472211e008571139dbc1edc89495",
33
+ "src/mask.mjs": "sha256-e75132137b7c22fb90332662de799c0d4dfeb269af31e531779d2b1883127b29",
34
+ "src/mcp.mjs": "sha256-43ab490d7311acf6daf1ee0f89b54894664181aae2cc407a8bb5a10d59f5631b",
35
+ "src/narrative.mjs": "sha256-08f63a33799752e409ac3bf3b55d45e0d54f4bb42165a5972eaea87f3eecbcc8",
36
+ "src/prompt.mjs": "sha256-ad8ac6a17d5201983b9885260332f19498d62bf1c742f370e281a2bc58e2271c",
37
+ "src/prompt.test.mjs": "sha256-70bcfed0de66602dd30d18456625aa7baa205e77807ca35f1d070bca0db95b2a",
38
+ "src/remote-config.mjs": "sha256-34943e75edca172d80b9698de94a80c25ce923538000d35ccb3e88c5e3ee4e04",
39
+ "src/render.mjs": "sha256-1300efab5a1bb53e8816539ddea48ac57e6ac8f991e07c06da9ae0e2da82745d",
40
+ "src/send.mjs": "sha256-5ec91f7d886fe5c16977983c388be7d1ccedfbe8c19cf0af1b6281c249ce9329",
41
+ "src/settings.mjs": "sha256-49b3598f03bc302ca1496f853656888ca7c21e15c19343357ece26c5b3b08ba7",
42
+ "src/sources/claude-code.mjs": "sha256-ab72ecaa26828d47c9f64ed35a43b8be9456ed9002216d2b2662f7a6866a63c5",
43
+ "src/sources/codex-cli.mjs": "sha256-26072f5de2b3c38f0f5aaecd691a789f9290529e7023676337478e6e68ca7012",
44
+ "src/sources/copilot-cli.mjs": "sha256-391b0f72bb472b4c62b939bd78e2db08c28ffbbdcad5ea85b5fec451af11b7c1",
45
+ "src/sources/cowork.mjs": "sha256-339d1be932bde7942230cd8c8963171c3067b57ef7f5f40097bd0433f8d62372",
46
+ "src/sources/events-file.mjs": "sha256-e6c3667c5a53c6d37697e418a030e7b61871e7f49d445903bbc980fdbc331b3a",
47
+ "src/sources/gemini-cli.mjs": "sha256-bdd8873e0b829996fae6cdb7e04b6b1e4ba39d7f200c4dd3ee3245d85db5b7c0",
48
+ "src/sources/hermes.mjs": "sha256-133d63b17e68c11f50d3f061bc8f5f037c13911e1a52c48921125a0ac000123d",
49
+ "src/store.mjs": "sha256-0e13d15eb892a04248b86f9b765660c4bf53b120233c1e8ec8ad665fe7bdfe00",
50
+ "src/tasks.mjs": "sha256-9d3ad22f80b1142bc101d42821a11c6ff2df7fa662316f2ab94ca3db87a4405a",
51
+ "src/tool-name.mjs": "sha256-b265135dd76ea2fab3de09e9bbe02c40cfaaad1a33465d6fb1b6670929389405",
52
+ "templates/report-v3.reference.html": "sha256-c96e03cfac104a5c13c9f38e60c0ff3b027f12f93b1b0de43b6e8be85ef97e09",
53
+ "web/assets/logo-clubs-white.png": "sha256-96a96b6a94443c3407b5ea7def5a1e990da9c206c628125574652c1d35a35aa3",
54
+ "web/assets/logo-clubs-white.svg": "sha256-4daa1bae7146e379c3ab0c38a48e6e7d98ecc7fdbe03ef252f143a0aaabbfb35",
55
+ "web/assets/logo-clubs-white.svg.tmp": "sha256-8231a68632766e44ce59dbb3a996896e18b515bdded31775faee238b10b15565",
56
+ "web/assets/logo-clubs.svg": "sha256-8231a68632766e44ce59dbb3a996896e18b515bdded31775faee238b10b15565",
57
+ "web/assets/qr.js": "sha256-b29cc7f282db6e16e70c7478542548869bb1e100454f5969c56a6fdec533cee0",
58
+ "web/assets/sidebar.js": "sha256-ee943629d6cf22d19d10670805d5ecbe403759b2cd7bcef4733b8645c76bb7d2",
59
+ "web/assets/twemoji.js": "sha256-542608bb2f7729ff344ef3328519a2ab56aad976b1b303e3a9e2f0fd97a1f3b3",
60
+ "web/cards/achievements.html": "sha256-b1cd85949a6e64d1198970e1fa216b30f69f0f30c34189c5f7a6335cab07ae74",
61
+ "web/cards/catalog.json": "sha256-a2093c0add8922b3d48c0dd934fefa05952d6d4470e10068ae73a8d9575e87dd",
62
+ "web/email/hero-v3.reference.html": "sha256-98b19a7998a823eaae259c34fa7d491e6f9ac96597120cc38093b4a85836ae52",
63
+ "web/home/index.html": "sha256-5adce8153479e59072b6b365fe14c4c0003fb0c18d143c58a19ea073c8e93445",
64
+ "web/preview.json": "sha256-78f89be7eef48d2a47e484e16e35890a60ef77278ae1a7fa9b2360dfbe9eb883",
65
+ "web/qr-selftest.html": "sha256-242025d701b4f19a56dd6024fa9bc3f7f3b86de5bf7eaeef2411364b8a483c44",
66
+ "web/reports/edit.html": "sha256-d7046fed8537d3bb266ab62c077172c591fcd6a83e44161448d127b4b0018113",
67
+ "web/reports/edit.reference.html": "sha256-77ea97dbf44393bb8f4c1d56c32ab7bed63686f7d6dabdeb551ce8c877ea3516",
68
+ "web/reports/index.html": "sha256-61701a89185d786b341ee596cc415b40ce25be9a7f23d9aa9df5370fb069b794",
69
+ "web/settings/index.html": "sha256-dc4a2e16bccc984bf8d752f8f6f6a6a04a4a01fa4cab7a16b53c139a10a98400",
70
+ "web/share/cards-reference.html": "sha256-370a616a5e158ddc2da449adf91e3312a6a3ccf8a7510feeefea3a157b1b3f85",
71
+ "web/share/passport.html": "sha256-fd632d17386f3869f74707885cb7d12315c5547bbc26a6601ac6756bd17de18b",
72
+ "web/share/teianbako.html": "sha256-b45457a41c05e19a8c2a877b83c60df9e075ddb1a58de964b906fb0249eafdc1",
73
+ "web/signup/index.html": "sha256-a24ff00ff4d94fb234899d1c43d5537f0af069500dad4039f3656d435c1f32c0",
74
+ "web/tasks/index.html": "sha256-b6411fa8e671c9e6712cf02548e8fc1bf500387143fafe5caad2c2ba7ebada73",
75
+ "web/verify/index.html": "sha256-7a9c85bc8a06c8282bda133bb5421f67765420319c9d47935e9c2b12216f2b65"
76
+ }
77
+ }
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # clubs-nippo — AI業務日報
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AIエージェントの実行ログから、前日ぶんの日報を自動生成してメール送信する CLI ツール。
4
+
5
+ - **pull 型**: 生ログはあなたの端末を出ない。集計値とマスク済み本文だけを送る
6
+ - **依存ゼロ**: Node 20+ の標準機能のみ。`npm install` 不要
7
+ - **Lemma 証明**: `LEMMA_API_KEY` を設定すれば ✓(改ざん検知)が付く
8
+
9
+ ## セットアップ
10
+
11
+ CLUBS の接続画面が発行するコマンドをそのまま貼る:
12
+
13
+ ```bash
14
+ npx clubs-nippo@latest init --url=<取込URL> --code=<接続コード>
15
+ ```
16
+
17
+ 接続コードは 10 分・1 回限り有効で、init がその場で連携トークンに引き換える
18
+ (トークンを画面からコピペしない)。期限が切れたら接続画面で再発行する。
19
+ 従来の `--token=<連携トークン>` 直接指定(再発行モーダル)も引き続き使える。
20
+
21
+ 設定は `~/.clubs-nippo/.env` に保存される。init が crontab 行も表示するので、`crontab -e` に貼れば毎朝自動実行される。
22
+
23
+ AI が一度も動かなかった日は日報にならない(メールも来ない・コンソールにも並ばない)。
24
+ 土日や休みの日を曜日で判定しているのではなく、稼働そのものを見ている——日曜に働けば
25
+ 日報は出るし、平日に一度も動かなければ出ない。
26
+
27
+ ## 使い方
28
+
29
+ ```bash
30
+ npx clubs-nippo@latest # 前日ぶんを生成して送信
31
+ npx clubs-nippo@latest --dry-run # 送信せず out/ に HTML を書くだけ
32
+ npx clubs-nippo@latest --date=2026-07-13 # 対象日を指定
33
+ ```
34
+
35
+ ## MCP(AIクライアントから登録・疎通確認)
36
+
37
+ 同じバイナリが MCP サーバー(stdio)としても動く。Claude Code / Cursor 等に
38
+ 登録すると、対話の中で CLUBS への登録と連携状態の確認ができる:
39
+
40
+ ```bash
41
+ claude mcp add clubs -- npx -y clubs-nippo@latest mcp
42
+ ```
43
+
44
+ ツールは `clubs_register`(接続画面が発行した取込URL・連携トークンで登録)と
45
+ `clubs_link_status`(設定・サーバ疎通・トークン有効性)の2本だけ。日報の
46
+ 取り込み・シールの発行は MCP からはできない(判定・発行はサーバ専権)。
47
+
48
+ ## 環境変数
49
+
50
+ **CLUBS 接続時(`init` 済み)は下記の設定は不要。** 証明の登録も日報メールの送信も CLUBS がサーバ側で行う。ローカルに置く鍵は生成用の `OPENROUTER_API_KEY`(OpenRouter を使う場合のみ・任意)だけ。以下は単体利用(メールデモ)向け:
51
+
52
+ | 変数 | 説明 |
53
+ |---|---|
54
+ | `EMAIL_API_KEY` | Resend の API キー |
55
+ | `EMAIL_FROM` | 送信元アドレス |
56
+ | `EMAIL_TO` | 宛先(カンマ区切りで複数可) |
57
+ | `LEMMA_API_KEY` | Lemma の API キー(64桁16進)。設定すると ✓ が付く(**CLUBS 接続時は不使用**=照合はサーバが行う) |
58
+ | `OWNER_NAME` | オーナー名(任意) |
59
+
60
+ 詳細は [`.env.example`](.env.example) を参照。
61
+
62
+ ## Provenance
63
+
64
+ Each source file carries a copyright header. `PROVENANCE.json` (generated at
65
+ publish time) pins the exact commit and the SHA-256 of every distributed file,
66
+ so a consumer can verify which revision they are using.
67
+ If you incorporate this code, please retain the copyright headers.
68
+
69
+ ## ライセンス
70
+
71
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // clubs-nippo — AI業務日報 (CLUBS)
3
+ // Copyright (c) 2026 FRAME00 Inc.
4
+ // SPDX-License-Identifier: MIT
5
+ // https://github.com/frame00/clubs-nippo
6
+
7
+ import('./clubs-nippo.mjs');
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+ // clubs-nippo — AI業務日報 (CLUBS)
3
+ // Copyright (c) 2026 FRAME00 Inc.
4
+ // SPDX-License-Identifier: MIT
5
+ // https://github.com/frame00/clubs-nippo
6
+
7
+ import { createServer } from "node:http";
8
+ import { readFile } from "node:fs/promises";
9
+ import { extname, join, normalize } from "node:path";
10
+ import { loadConfig, ROOT } from "../src/config.mjs";
11
+ import { loadDraft, loadApproved, saveApproved, listReports, monthSummary, passport } from "../src/store.mjs";
12
+ import { registerProof, documentHash } from "../src/lemma.mjs";
13
+ import { CATALOG, PERMISSIONS, loadCards, hasQuote } from "../src/cards.mjs";
14
+ import { readSettings, writeOwnerName } from "../src/settings.mjs";
15
+ import {
16
+ accumulateFromNarrative,
17
+ listTasks,
18
+ listProposals,
19
+ setTaskStatus,
20
+ setProposalDecision,
21
+ } from "../src/tasks.mjs";
22
+
23
+ /**
24
+ * 日報の Web 画面(一覧・編集・検証)を出し、承認エンドポイントを提供する。
25
+ * Node 標準の http だけで書く。依存パッケージは増やさない。
26
+ *
27
+ * GET /reports/ 一覧
28
+ * GET /reports/edit.html?date=... 編集
29
+ * GET /verify/ 検証(証明書型)
30
+ * GET /api/reports 一覧データ
31
+ * GET /api/reports/:date 下書き
32
+ * POST /api/reports/:date/record 編集版を記録(内容が変わったときだけ登録)
33
+ * GET /api/passport パスポート(累計)
34
+ * GET /api/settings 表示名・収集元
35
+ * PUT /api/settings オーナー名だけ更新(AI名は変更不可)
36
+ * GET /api/tasks 申し送りから溜めたタスク
37
+ * GET /api/proposals 気づき・提案(stats はサーバ集計)
38
+ * POST /api/tasks/:id/status open/done 切替
39
+ * POST /api/proposals/:id/decision 承認/却下/保留を記録(実行はしない)
40
+ */
41
+
42
+ const PORT = Number(process.env.PORT ?? 8899);
43
+ const WEB = join(ROOT, "web");
44
+
45
+ const MIME = {
46
+ ".html": "text/html; charset=utf-8",
47
+ ".css": "text/css; charset=utf-8",
48
+ ".js": "text/javascript; charset=utf-8",
49
+ ".json": "application/json; charset=utf-8",
50
+ ".svg": "image/svg+xml",
51
+ };
52
+
53
+ const json = (res, status, body) => {
54
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
55
+ res.end(JSON.stringify(body));
56
+ };
57
+
58
+ const readBody = (req) =>
59
+ new Promise((resolve, reject) => {
60
+ const chunks = [];
61
+ req.on("data", (c) => chunks.push(c));
62
+ req.on("end", () => {
63
+ try {
64
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
65
+ } catch (e) {
66
+ reject(e);
67
+ }
68
+ });
69
+ req.on("error", reject);
70
+ });
71
+
72
+ const serveStatic = async (res, urlPath) => {
73
+ // web/ の外に出させない。
74
+ const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
75
+ const path = join(WEB, rel.endsWith("/") ? `${rel}index.html` : rel);
76
+ if (!path.startsWith(WEB)) return json(res, 403, { error: "Forbidden" });
77
+
78
+ try {
79
+ const body = await readFile(path);
80
+ res.writeHead(200, {
81
+ "Content-Type": MIME[extname(path)] ?? "application/octet-stream",
82
+ // 毎回サーバに問い合わせさせる(変わっていなければ 304 で軽い)。
83
+ //
84
+ // サイドバーは assets/sidebar.js が生成するため、古い JS がキャッシュに残ると
85
+ // リンクの href が付いていない状態のまま表示され、「押しても飛ばない」ように見える。
86
+ // 実際にこれで時間を溶かした。版番号(?v=2)を手で上げる方式は、上げ忘れれば同じ事故が
87
+ // 起きるので採らない。
88
+ "Cache-Control": "no-cache",
89
+ });
90
+ res.end(body);
91
+ } catch {
92
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
93
+ res.end("Not found");
94
+ }
95
+ };
96
+
97
+ /**
98
+ * 編集版を記録する。
99
+ *
100
+ * 日報は送信時に自動で Lemma へ登録され、✓ が付いている。ここは「本文を整えて記録し直す」面。
101
+ *
102
+ * 意味のある証明だけ増やす:
103
+ * 編集後の docHash が自動登録ぶんと同じなら、登録し直さない。中身が変わっていないのに
104
+ * 証明を積んでも、証明の数が増えるだけで意味は増えない。
105
+ *
106
+ * 受け取るのは文章だけ(workBlocks の title/text・handoff・selfNote)。
107
+ * 集計値(aggregates)はリクエストから一切読まない。必ず下書きのロック値を使う。
108
+ * 数字をクライアントから受けられるようにすると「盛れない」が崩れる。
109
+ */
110
+ const record = async (res, date, body, config) => {
111
+ const draft = loadDraft(date);
112
+ if (!draft) return json(res, 404, { error: `下書きがありません: ${date}` });
113
+
114
+ // 文章だけを取り出す。数字が混ざっていても捨てる。
115
+ const edited = {
116
+ workBlocks: (Array.isArray(body.workBlocks) ? body.workBlocks : []).map((b, i) => ({
117
+ title: String(b?.title ?? draft.workBlocks[i]?.title ?? ""),
118
+ text: String(b?.text ?? draft.workBlocks[i]?.text ?? ""),
119
+ // tool と metric はログ由来。編集させない。
120
+ tool: draft.workBlocks[i]?.tool ?? "",
121
+ metric: draft.workBlocks[i]?.metric ?? "",
122
+ })),
123
+ // 下書きの handoff は文字列。docHash / 描画は {title, text} の形を使うので組み直す。
124
+ handoff: {
125
+ title: draft.handoffTitle ?? "明日にまわすもの",
126
+ text: typeof body.handoff === "string" ? body.handoff : (draft.handoff ?? ""),
127
+ },
128
+ selfNote: typeof body.selfNote === "string" ? body.selfNote : draft.selfNote,
129
+ };
130
+
131
+ // docHash はロックされた集計値+編集後の文章から出す。CLI と同じ関数を使う。
132
+ const activity = {
133
+ date: draft.date,
134
+ aggregates: draft.aggregates, // ← ロック値。body からは取らない
135
+ agentName: draft.agentName,
136
+ };
137
+ const narrative = { ...edited, insights: draft.insights ?? [] };
138
+
139
+ // 送信時に自動登録された版と同じ内容なら、登録し直さない。
140
+ const nextHash = documentHash(activity, narrative);
141
+ const already = loadApproved(date)?.proof ?? draft.proof ?? null;
142
+ const unchanged = already?.status === "verified" && already.docHash === nextHash;
143
+
144
+ const proof = unchanged ? already : await registerProof(activity, narrative, config);
145
+
146
+ const saved = saveApproved({
147
+ ...draft,
148
+ ...narrative,
149
+ recordedAt: new Date().toISOString(),
150
+ proof,
151
+ // パスポートに公開するのは記録した版。
152
+ published: true,
153
+ });
154
+
155
+ // 承認済み本文から、申し送り→タスク/気づき・提案→提案 を蓄積する。
156
+ // 確定した本文だけを源にする(下書きからは拾わない)。繰越同定はここで走る。
157
+ accumulateFromNarrative(narrative, saved.date);
158
+
159
+ return json(res, 200, {
160
+ // Lemma に登録できていなければ ✓ は付けない。記録自体は成立させる。
161
+ status: proof.status === "verified" ? "recorded" : "pending",
162
+ docHash: proof.docHash,
163
+ verifyUrl: proof.verifyUrl,
164
+ reason: proof.reason,
165
+ // 内容が変わっていなければ新しい証明は積んでいない。それを隠さない。
166
+ reregistered: !unchanged,
167
+ date: saved.date,
168
+ });
169
+ };
170
+
171
+ const server = createServer(async (req, res) => {
172
+ const url = new URL(req.url, `http://${req.headers.host}`);
173
+ const path = url.pathname;
174
+ const config = loadConfig({});
175
+
176
+ try {
177
+ if (path === "/api/reports" && req.method === "GET") {
178
+ const reports = listReports();
179
+ const today = new Date(Date.now() + config.tzOffsetMin * 60_000)
180
+ .toISOString()
181
+ .slice(0, 10);
182
+ return json(res, 200, { reports, month: monthSummary(reports, today, loadCards()) });
183
+ }
184
+
185
+ if (path === "/api/cards" && req.method === "GET") {
186
+ const held = loadCards();
187
+ const earned = new Set([
188
+ ...held.achievements.map((a) => a.id),
189
+ ...held.titles.map((t) => t.id),
190
+ ]);
191
+
192
+ // カタログは全部返す。持っていないものは locked として出す
193
+ // (何を目指せるかが見えないと、実績が積み上がる意味が伝わらない)。
194
+ const cards = CATALOG.map((c) => ({
195
+ id: c.id,
196
+ name: c.name,
197
+ subtitle: c.subtitle,
198
+ rarity: c.rarity,
199
+ kind: c.kind,
200
+ domain: c.domain ?? null,
201
+ tw: c.tw,
202
+ effectPermission: c.effectPermission ?? null,
203
+ // AI の引用は EPIC 以上だけ。それ未満は副題のみ。
204
+ quote: hasQuote(c.rarity) ? (c.quote ?? null) : null,
205
+ earned: earned.has(c.id),
206
+ earnedAt:
207
+ held.titles.find((t) => t.id === c.id)?.earnedAt ??
208
+ held.achievements.filter((a) => a.id === c.id).at(-1)?.date ??
209
+ null,
210
+ }));
211
+
212
+ return json(res, 200, {
213
+ cards,
214
+ permissions: PERMISSIONS.map((p) => ({
215
+ id: p.id,
216
+ name: p.name,
217
+ subtitle: p.subtitle,
218
+ scope: p.scope,
219
+ // 「解放可能」であって「付与済み」ではない。付与は人間が押す。
220
+ unlockable: held.permissionsUnlockable.some((u) => u.id === p.id),
221
+ })),
222
+ });
223
+ }
224
+
225
+ if (path === "/api/passport" && req.method === "GET") {
226
+ const s = readSettings(config);
227
+ return json(res, 200, passport(listReports(), s.displayName, s.agentName));
228
+ }
229
+
230
+ if (path === "/api/settings" && req.method === "GET") {
231
+ return json(res, 200, readSettings(config));
232
+ }
233
+
234
+ if (path === "/api/settings" && req.method === "PUT") {
235
+ const body = await readBody(req);
236
+ // 受け取るのはオーナー名だけ。AI名は収集元が決めるので、送られてきても無視する。
237
+ // ここを書き換えられるようにすると、他のAIの作業を自分の実績にできてしまう。
238
+ if (typeof body.ownerName !== "string") {
239
+ return json(res, 400, { error: "ownerName は文字列で送ってください。" });
240
+ }
241
+ writeOwnerName(body.ownerName.trim());
242
+ return json(res, 200, readSettings(loadConfig({})));
243
+ }
244
+
245
+ // ── タスク・提案箱(日報の申し送り/気づきを蓄積したビュー) ──────
246
+ // 数字・集計は out/ の確定データから出す。body の数値は信用しない。
247
+ if (path === "/api/tasks" && req.method === "GET") {
248
+ return json(res, 200, { items: listTasks() });
249
+ }
250
+
251
+ if (path === "/api/proposals" && req.method === "GET") {
252
+ // stats はサーバ集計(total / approved / approvalRate / streakDays)。
253
+ return json(res, 200, listProposals());
254
+ }
255
+
256
+ const taskStatusMatch = path.match(/^\/api\/tasks\/([0-9a-f]{8,})\/status$/);
257
+ if (taskStatusMatch && req.method === "POST") {
258
+ const body = await readBody(req);
259
+ const today = new Date(Date.now() + config.tzOffsetMin * 60_000).toISOString().slice(0, 10);
260
+ const item = setTaskStatus(taskStatusMatch[1], body.status, today);
261
+ return item
262
+ ? json(res, 200, { item })
263
+ : json(res, 400, { error: "status は open|done、id は既存のものを送ってください。" });
264
+ }
265
+
266
+ const propDecisionMatch = path.match(/^\/api\/proposals\/([0-9a-f]{8,})\/decision$/);
267
+ if (propDecisionMatch && req.method === "POST") {
268
+ const body = await readBody(req);
269
+ // 決定の記録のみ。実行はしない。承認者は監査用に残す。
270
+ const item = setProposalDecision(
271
+ propDecisionMatch[1],
272
+ body.decision,
273
+ readSettings(config).displayName,
274
+ new Date().toISOString(),
275
+ );
276
+ return item
277
+ ? json(res, 200, { item })
278
+ : json(res, 400, { error: "decision は approved|rejected|hold、id は既存のものを送ってください。" });
279
+ }
280
+
281
+ // /record が正。/approve は旧名(既存の編集画面が叩いていたので受け続ける)。
282
+ const recordMatch = path.match(/^\/api\/reports\/(\d{4}-\d{2}-\d{2})\/(record|approve)$/);
283
+ if (recordMatch && req.method === "POST") {
284
+ return record(res, recordMatch[1], await readBody(req), config);
285
+ }
286
+
287
+ const draftMatch = path.match(/^\/api\/reports\/(\d{4}-\d{2}-\d{2})$/);
288
+ if (draftMatch && req.method === "GET") {
289
+ const date = draftMatch[1];
290
+ const doc = loadApproved(date) ?? loadDraft(date);
291
+ return doc
292
+ ? json(res, 200, doc)
293
+ : json(res, 404, { error: `日報がありません: ${date}` });
294
+ }
295
+
296
+ // 検証ページが docHash を引く先(Lemma の GET が出るまでのローカル代替)。
297
+ const verifyMatch = path.match(/^\/api\/documents\/(0x[0-9a-f]+)$/i);
298
+ if (verifyMatch && req.method === "GET") {
299
+ const hash = verifyMatch[1].toLowerCase();
300
+ const hit = listReports().find((r) => r.docHash?.toLowerCase() === hash);
301
+ if (!hit) return json(res, 404, { error: "not found" });
302
+ const rec = loadApproved(hit.date);
303
+ // Lemma の GET /v1/documents/:docHash と同じ語彙で返す(registered / anchored / pending)。
304
+ // 一覧の内部表現は "recorded" だが、それをそのまま出すと証明書ページが未知の値として
305
+ // 扱い、登録済みの日報が「照合を待っています」に見えていた。
306
+ const status =
307
+ hit.status === "draft" || hit.status === "pending"
308
+ ? "pending"
309
+ : hit.status === "anchored"
310
+ ? "anchored"
311
+ : "registered";
312
+ return json(res, 200, {
313
+ status,
314
+ registeredAt: rec.approvedAt,
315
+ issuerId: rec.agentName,
316
+ subjectId: config.email.to[0] ?? "",
317
+ docHash: rec.proof.docHash,
318
+ onchainTxHash: rec.proof.txHash ?? undefined,
319
+ date: rec.date,
320
+ });
321
+ }
322
+
323
+ // 着地はホーム。日報一覧は /reports/ のまま。
324
+ if (req.method === "GET") return serveStatic(res, path === "/" ? "/home/" : path);
325
+
326
+ return json(res, 405, { error: "Method not allowed" });
327
+ } catch (e) {
328
+ console.error(e);
329
+ return json(res, 500, { error: "Internal error" });
330
+ }
331
+ });
332
+
333
+ server.listen(PORT, () => {
334
+ console.log(`clubs-nippo web: http://localhost:${PORT}/reports/`);
335
+ console.log(` 一覧 http://localhost:${PORT}/reports/`);
336
+ console.log(` 編集 http://localhost:${PORT}/reports/edit.html?date=YYYY-MM-DD`);
337
+ console.log(` 検証 http://localhost:${PORT}/verify/?demo=1`);
338
+ });