@clubs.place/cli 0.1.18 → 0.1.24

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 (53) hide show
  1. package/PROVENANCE.json +53 -47
  2. package/README.md +20 -11
  3. package/bin/clubs-nippo +0 -5
  4. package/bin/clubs-nippo-web.mjs +0 -5
  5. package/bin/clubs-nippo.mjs +1 -526
  6. package/bin/clubs.mjs +643 -0
  7. package/docs/events-schema.md +1 -1
  8. package/package.json +6 -5
  9. package/src/aggregate.mjs +32 -5
  10. package/src/aggregate.slots.test.mjs +23 -0
  11. package/src/autorun.mjs +1 -6
  12. package/src/autorun.test.mjs +2 -7
  13. package/src/cards.mjs +0 -5
  14. package/src/collect.mjs +0 -5
  15. package/src/config.mjs +10 -6
  16. package/src/generators/claude.mjs +0 -5
  17. package/src/generators/cli.mjs +0 -5
  18. package/src/generators/codex.mjs +0 -5
  19. package/src/generators/copilot.mjs +0 -5
  20. package/src/generators/gemini.mjs +0 -5
  21. package/src/generators/openai-compat.mjs +0 -5
  22. package/src/generators/openrouter.mjs +0 -5
  23. package/src/history.mjs +2 -5
  24. package/src/ingest.mjs +0 -5
  25. package/src/lemma.docHash.test.mjs +0 -5
  26. package/src/lemma.mjs +0 -5
  27. package/src/mask.mjs +0 -5
  28. package/src/mcp.mjs +26 -9
  29. package/src/narrative.blockRepos.test.mjs +48 -0
  30. package/src/narrative.mjs +47 -8
  31. package/src/profiles.mjs +121 -0
  32. package/src/profiles.test.mjs +73 -0
  33. package/src/prompt.mjs +99 -24
  34. package/src/prompt.test.mjs +87 -20
  35. package/src/remote-config.mjs +5 -6
  36. package/src/render.mjs +1 -5
  37. package/src/routines.mjs +61 -0
  38. package/src/routines.test.mjs +29 -0
  39. package/src/send.mjs +0 -5
  40. package/src/settings.mjs +0 -5
  41. package/src/sources/claude-code.mjs +0 -5
  42. package/src/sources/codex-cli.mjs +0 -5
  43. package/src/sources/copilot-cli.mjs +0 -5
  44. package/src/sources/cowork.mjs +0 -5
  45. package/src/sources/events-file.mjs +0 -5
  46. package/src/sources/gemini-cli.mjs +0 -5
  47. package/src/sources/hermes.mjs +0 -5
  48. package/src/store.mjs +0 -5
  49. package/src/tasks.mjs +0 -5
  50. package/src/tool-name.mjs +0 -5
  51. package/web/assets/qr.js +0 -5
  52. package/web/assets/sidebar.js +0 -5
  53. package/web/assets/twemoji.js +0 -5
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@clubs.place/cli",
3
- "version": "0.1.18",
4
- "description": "AI\u696d\u52d9\u65e5\u5831 \u2014 Claude Code \u306e\u5b9f\u884c\u30ed\u30b0\u304b\u3089\u524d\u65e5\u3076\u3093\u306e\u65e5\u5831\u30e1\u30fc\u30eb\u3092\u751f\u6210\u3057\u3066\u9001\u308b\uff08pull\u578b\u30fb\u672c\u6587\u751f\u6210\u306f\u30e6\u30fc\u30b6\u30fc\u5074LLM\u30fb\u751f\u30ed\u30b0\u306f\u7aef\u672b\u3092\u51fa\u306a\u3044\uff09",
3
+ "version": "0.1.24",
4
+ "description": "AI業務日報 — Claude Code の実行ログから前日ぶんの日報メールを生成して送る(pull型・本文生成はユーザー側LLM・生ログは端末を出ない)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
- "clubs-place": "bin/clubs-nippo",
9
8
  "clubs-nippo": "bin/clubs-nippo",
9
+ "clubs": "bin/clubs-nippo",
10
+ "clubs-place": "bin/clubs-nippo",
10
11
  "clubs-place-cli": "bin/clubs-nippo"
11
12
  },
12
13
  "files": [
@@ -31,8 +32,8 @@
31
32
  "node": ">=20"
32
33
  },
33
34
  "scripts": {
34
- "nippo": "node ./bin/clubs-nippo.mjs",
35
- "dry": "node ./bin/clubs-nippo.mjs --dry-run",
35
+ "nippo": "node ./bin/clubs.mjs",
36
+ "dry": "node ./bin/clubs.mjs --dry-run",
36
37
  "demo": "node scripts/gen-preview.mjs && node scripts/bundle-demo.mjs",
37
38
  "test": "node --test",
38
39
  "prepublishOnly": "node scripts/gen-provenance.mjs"
package/src/aggregate.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * 収集元が集めた生イベントから、日報に載せる数字を出す。
8
3
  *
@@ -26,6 +21,37 @@ const estimateActiveMinutes = (timestampsMs, idleGapMin) => {
26
21
  return Math.round(total / 60_000);
27
22
  };
28
23
 
24
+ /**
25
+ * 動いていた時間帯(本人 2026-10-17:時間を二重に数えない)。1日を5分ごとの 288 枠に分け、
26
+ * イベントのある枠と、間が idleGapMin 以内のイベントどうしのあいだの枠に印を付ける
27
+ * (estimateActiveMinutes と同じ考え方)。CLUBS は AI ごとの枠を重ねて(和集合で)
28
+ * その日の時間を出す。288 ビットを 16 進 72 文字で送る。時刻は端末の時刻(e.time)。
29
+ */
30
+ export const SLOT_MIN = 5;
31
+ export const activeSlots = (sorted, idleGapMin) => {
32
+ const bits = new Array(288).fill(0);
33
+ const slotOf = (t) => {
34
+ const [h, m] = String(t ?? "").split(":").map(Number);
35
+ if (!Number.isFinite(h) || !Number.isFinite(m)) return -1;
36
+ return Math.min(287, Math.floor((h * 60 + m) / SLOT_MIN));
37
+ };
38
+ const capMs = idleGapMin * 60_000;
39
+ for (let i = 0; i < sorted.length; i += 1) {
40
+ const s = slotOf(sorted[i].time);
41
+ if (s < 0) continue;
42
+ bits[s] = 1;
43
+ const prev = sorted[i - 1];
44
+ if (!prev || sorted[i].ms - prev.ms > capMs) continue;
45
+ const p = slotOf(prev.time);
46
+ if (p < 0 || p > s) continue;
47
+ for (let k = p; k <= s; k += 1) bits[k] = 1;
48
+ }
49
+ let hex = "";
50
+ for (let i = 0; i < 288; i += 4)
51
+ hex += ((bits[i] << 3) | (bits[i + 1] << 2) | (bits[i + 2] << 1) | bits[i + 3]).toString(16);
52
+ return hex;
53
+ };
54
+
29
55
  export const computeAggregates = ({ events, sessions, idleGapMin }) => {
30
56
  // 複数の収集元を合算する場合、イベントは時系列で混ざる。
31
57
  // 実働はマージ後の全イベント列から出す(収集元ごとに出して足すと、
@@ -101,5 +127,6 @@ export const computeAggregates = ({ events, sessions, idleGapMin }) => {
101
127
  projects,
102
128
  firstAt: sorted.at(0)?.time ?? null,
103
129
  lastAt: sorted.at(-1)?.time ?? null,
130
+ activeSlots: activeSlots(sorted, idleGapMin),
104
131
  };
105
132
  };
@@ -0,0 +1,23 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { activeSlots } from "./aggregate.mjs";
4
+
5
+ const ev = (time) => {
6
+ const [h, m] = time.split(":").map(Number);
7
+ return { time, ms: Date.UTC(2026, 9, 16, h, m) };
8
+ };
9
+ const bits = (hex) =>
10
+ [...hex].flatMap((c) => parseInt(c, 16).toString(2).padStart(4, "0").split("").map(Number));
11
+
12
+ test("イベントの枠と、間が idleGap 以内のあいだの枠に印", () => {
13
+ const b = bits(activeSlots([ev("10:00"), ev("10:12"), ev("11:00")], 15));
14
+ assert.equal(b.length, 288);
15
+ const on = b.flatMap((v, i) => (v ? [i] : []));
16
+ // 10:00–10:12 は 120,121,122 枠・11:00 は 132 枠(10:12→11:00 は 15 分超でつながない)
17
+ assert.deepEqual(on, [120, 121, 122, 132]);
18
+ });
19
+
20
+ test("時刻の読めないイベントは数えない・空の日は全部 0", () => {
21
+ assert.equal(activeSlots([], 15), "0".repeat(72));
22
+ assert.equal(activeSlots([{ time: "", ms: 0 }], 15), "0".repeat(72));
23
+ });
package/src/autorun.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { platform as osPlatform } from "node:os";
7
2
 
8
3
  /**
@@ -20,6 +15,6 @@ export const tickCommand = (npx, plat = osPlatform()) => [
20
15
  ...(plat === "darwin" ? ["/usr/bin/caffeinate", "-s", "-i", "--"] : []),
21
16
  npx,
22
17
  "-y",
23
- "clubs-nippo@latest",
18
+ "@clubs.place/cli@latest",
24
19
  "tick",
25
20
  ];
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { test } from "node:test";
7
2
  import assert from "node:assert/strict";
8
3
  import { tickCommand } from "./autorun.mjs";
@@ -21,14 +16,14 @@ test("macOS では caffeinate で包む(実行中のスリープを止める
21
16
  "--",
22
17
  NPX,
23
18
  "-y",
24
- "clubs-nippo@latest",
19
+ "@clubs.place/cli@latest",
25
20
  "tick",
26
21
  ]);
27
22
  });
28
23
 
29
24
  test("macOS 以外では包まない(caffeinate が無い)", () => {
30
25
  for (const plat of ["linux", "freebsd", "win32"]) {
31
- assert.deepEqual(tickCommand(NPX, plat), [NPX, "-y", "clubs-nippo@latest", "tick"]);
26
+ assert.deepEqual(tickCommand(NPX, plat), [NPX, "-y", "@clubs.place/cli@latest", "tick"]);
32
27
  }
33
28
  });
34
29
 
package/src/cards.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { APP_HOME } from "./config.mjs";
package/src/collect.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import claudeCode from "./sources/claude-code.mjs";
7
2
  import cowork from "./sources/cowork.mjs";
8
3
  import eventsFile from "./sources/events-file.mjs";
package/src/config.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, existsSync } from "node:fs";
7
2
  import { homedir } from "node:os";
8
3
  import { join, dirname } from "node:path";
@@ -24,6 +19,12 @@ export const APP_HOME =
24
19
  ? ROOT
25
20
  : join(homedir(), ".clubs-nippo"));
26
21
 
22
+ /**
23
+ * .env から読み込んだキー(profiles.mjs が子プロセスに渡さないため)。
24
+ * 並べた AI を動かすとき、APP_HOME の鍵を引き継がせない。
25
+ */
26
+ export const LOADED_ENV_KEYS = [];
27
+
27
28
  /** .env は「まだ環境変数に無いキーだけ」入れる。実環境の変数が常に勝つ。 */
28
29
  const loadDotEnv = () => {
29
30
  const path = join(APP_HOME, ".env");
@@ -32,7 +33,10 @@ const loadDotEnv = () => {
32
33
  const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
33
34
  if (!m) continue;
34
35
  const value = m[2].replace(/^["']|["']$/g, "");
35
- if (process.env[m[1]] === undefined) process.env[m[1]] = value;
36
+ if (process.env[m[1]] === undefined) {
37
+ process.env[m[1]] = value;
38
+ LOADED_ENV_KEYS.push(m[1]);
39
+ }
36
40
  }
37
41
  };
38
42
  loadDotEnv();
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { makeCliGenerator } from "./cli.mjs";
7
2
 
8
3
  /**
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { execFile } from "node:child_process";
7
2
  import { promisify } from "node:util";
8
3
  import { ROOT } from "../config.mjs";
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { makeCliGenerator } from "./cli.mjs";
7
2
 
8
3
  /**
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { makeCliGenerator } from "./cli.mjs";
7
2
 
8
3
  /**
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { makeCliGenerator } from "./cli.mjs";
7
2
 
8
3
  /**
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * OpenAI 互換のカスタムエンドポイントで本文を書く(第3の生成バックエンド)。
8
3
  *
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * OpenRouter で本文を書く。
8
3
  *
package/src/history.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { APP_HOME } from "./config.mjs";
@@ -46,6 +41,8 @@ export const appendHistory = (activity) => {
46
41
  failedCalls: a.failedCalls,
47
42
  successRate: a.successRate,
48
43
  activeHours: a.activeHours,
44
+ // 関与プロジェクト(名前だけ・くり返しからルールの候補を出す材料・2026-10-04)。
45
+ projects: a.projects ?? [],
49
46
  toolCounts: a.toolCounts,
50
47
  toolCountsAm: a.toolCountsAm,
51
48
  toolCountsPm: a.toolCountsPm,
package/src/ingest.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * 生成した日報を web コンソール(clubsx)へ取り込ませる。
8
3
  *
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { test } from "node:test";
7
2
  import assert from "node:assert/strict";
8
3
  import { documentHash } from "./lemma.mjs";
package/src/lemma.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { createHash } from "node:crypto";
7
2
 
8
3
  // SDK の動的インポート。無い/読めないときは null → commitDeep/proofs.submit をスキップ。
package/src/mask.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  export const REDACT_TOKEN = "[[REDACTED]]";
7
2
 
8
3
  /**
package/src/mcp.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * CLUBS MCP サーバー(stdio・同一バイナリ同梱=全体設計 §6 / P0 §9)。
8
3
  *
@@ -16,6 +11,11 @@
16
11
  * - 出力はサーバ確定値・ローカル設定の定型フィールドのみ(自由文・他ユーザー
17
12
  * 由来のテキストを返さない=注入面の最小化)。
18
13
  *
14
+ * **終わる(CLUBS 判断 #26・2026-10-16)**: AI を CLUBS につなぐのはリモート MCP
15
+ * (接続画面の URL+許可)になり、この CLI は日報の自動化専用になった。前に
16
+ * 登録した人の AI が起動したとき黙って落ちないよう、しばらくは動かしたまま、
17
+ * 起動・道具の説明・返事のすべてで「つなぎ直して」を伝える(DEPRECATION)。
18
+ *
19
19
  * 依存ゼロ方針(このリポの流儀)のため SDK は使わず、MCP の stdio
20
20
  * トランスポート(JSON-RPC 2.0・1行1メッセージ)を最小実装する。
21
21
  * stdout はプロトコル専用。ログは必ず stderr へ。
@@ -30,6 +30,12 @@ import { readHistory } from "./history.mjs";
30
30
 
31
31
  const PROTOCOL_FALLBACK = "2025-06-18";
32
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
+
33
39
  const pkgVersion = () => {
34
40
  try {
35
41
  return JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
@@ -110,7 +116,9 @@ const TOOLS = [
110
116
  {
111
117
  name: "clubs_register",
112
118
  description:
113
- "この端末を CLUBS に登録する(clubs-nippo init と同じ設定を書く)。url と token は CLUBS の接続画面(/nippo/connections)が発行した値をそのまま渡すこと。登録後にサーバへ疎通確認する。",
119
+ "【終わります】" +
120
+ DEPRECATION +
121
+ " / この端末を CLUBS に登録する(clubs-nippo init と同じ設定を書く)。url と token は CLUBS の接続画面(/nippo/connections)が発行した値をそのまま渡すこと。登録後にサーバへ疎通確認する。",
114
122
  inputSchema: {
115
123
  type: "object",
116
124
  properties: {
@@ -132,12 +140,19 @@ const TOOLS = [
132
140
  {
133
141
  name: "clubs_link_status",
134
142
  description:
135
- "CLUBS との連携状態を返す(設定の有無・サーバ疎通・トークン有効性・ローカル最終記録日)。",
143
+ "【終わります】" +
144
+ DEPRECATION +
145
+ " / CLUBS との連携状態を返す(設定の有無・サーバ疎通・トークン有効性・ローカル最終記録日)。",
136
146
  inputSchema: { type: "object", properties: {} },
137
147
  },
138
148
  ];
139
149
 
140
- const callTool = async (name, args = {}) => {
150
+ export const callTool = async (name, args = {}) => ({
151
+ deprecated: DEPRECATION,
152
+ ...(await callToolInner(name, args)),
153
+ });
154
+
155
+ const callToolInner = async (name, args = {}) => {
141
156
  if (name === "clubs_register") {
142
157
  const url = String(args.url ?? "");
143
158
  const token = String(args.token ?? "");
@@ -184,7 +199,7 @@ const callTool = async (name, args = {}) => {
184
199
  ingest_url_host: parsed.host,
185
200
  server,
186
201
  next: server.token_valid
187
- ? "毎朝の自動配信は crontab に `npx -y clubs-nippo@latest tick` を登録(clubs-nippo init のヘルプ参照)"
202
+ ? "毎朝の自動配信は crontab に `npx -y @clubs.place/cli@latest tick` を登録(clubs-nippo init のヘルプ参照)"
188
203
  : "トークンがサーバで確認できませんでした。接続画面で再発行して register し直してください",
189
204
  };
190
205
  }
@@ -225,6 +240,7 @@ const replyError = (id, code, message) => {
225
240
 
226
241
  export const runMcpServer = async () => {
227
242
  const rl = createInterface({ input: process.stdin, terminal: false });
243
+ console.error(`[clubs-nippo mcp] ${DEPRECATION}`);
228
244
  console.error(
229
245
  `[clubs-nippo mcp] stdio server ready (tools: ${TOOLS.map((t) => t.name).join(", ")})`,
230
246
  );
@@ -248,6 +264,7 @@ export const runMcpServer = async () => {
248
264
  protocolVersion: params?.protocolVersion ?? PROTOCOL_FALLBACK,
249
265
  capabilities: { tools: { listChanged: false } },
250
266
  serverInfo: { name: "clubs-nippo", version: pkgVersion() },
267
+ instructions: DEPRECATION,
251
268
  });
252
269
  } else if (method === "ping") {
253
270
  reply(id, {});
@@ -0,0 +1,48 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { fallbackNarrative, withBlockRepos } from "./narrative.mjs";
4
+ import { documentHash } from "./lemma.mjs";
5
+
6
+ const activity = {
7
+ date: "2026-10-16",
8
+ aggregates: { totalCalls: 3, failedCalls: 0, activeHours: 1, projects: ["lemma", "clubsx"] },
9
+ events: [
10
+ { time: "09:00", name: "Edit", project: "lemma", summary: "a" },
11
+ { time: "09:10", name: "Bash", project: "clubsx", summary: "b" },
12
+ { time: "09:20", name: "Edit", project: "(案件未設定)", summary: "c" },
13
+ ],
14
+ };
15
+
16
+ test("行ごとのフォルダは行から外して blockRepos へ・集計の一覧にある名前だけ", () => {
17
+ const n = withBlockRepos(
18
+ {
19
+ workBlocks: [
20
+ { title: "LP", text: "x", repos: ["lemma", "secret"] },
21
+ { title: "名簿", text: "y", repos: [] },
22
+ { title: "表紙", text: "z" },
23
+ ],
24
+ },
25
+ activity,
26
+ );
27
+ assert.deepEqual(n.blockRepos, [["lemma"], [], []]);
28
+ assert.equal("repos" in n.workBlocks[0], false);
29
+ });
30
+
31
+ test("どの行にもフォルダが無ければ blockRepos を付けない", () => {
32
+ const n = withBlockRepos({ workBlocks: [{ title: "A", text: "x", repos: ["nope"] }] }, activity);
33
+ assert.equal("blockRepos" in n, false);
34
+ });
35
+
36
+ test("docHash は repos を外した行で計算する(前の版と同じ値)", () => {
37
+ const plain = { workBlocks: [{ title: "LP", text: "x" }], handoff: { title: "h", text: "t" }, selfNote: "s" };
38
+ const withRepos = withBlockRepos(
39
+ { ...plain, workBlocks: [{ title: "LP", text: "x", repos: ["lemma"] }] },
40
+ activity,
41
+ );
42
+ assert.equal(documentHash(activity, withRepos), documentHash(activity, plain));
43
+ });
44
+
45
+ test("予備の書き方はフォルダごとの行なので、そのフォルダ名を付ける(未設定は空)", () => {
46
+ const n = fallbackNarrative(activity);
47
+ assert.deepEqual(n.blockRepos, [["lemma"], ["clubsx"], []]);
48
+ });
package/src/narrative.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { buildPrompt } from "./prompt.mjs";
7
2
  import claudeGenerator from "./generators/claude.mjs";
8
3
  import codexGenerator from "./generators/codex.mjs";
@@ -125,7 +120,7 @@ export const fallbackNarrative = (activity, reason = "", generatorId = "") => {
125
120
 
126
121
  const summary = `${a.totalCalls} 件の操作を実行し、失敗は ${a.failedCalls} 件でした。`;
127
122
 
128
- return {
123
+ const narrative = {
129
124
  workBlocks,
130
125
  handoff: { title: "明日にまわすもの", text: "特にありません。" },
131
126
  insights: [],
@@ -136,6 +131,47 @@ export const fallbackNarrative = (activity, reason = "", generatorId = "") => {
136
131
  : summary,
137
132
  generator: reason ? `fallback(${reason})` : "fallback",
138
133
  };
134
+ // 予備の書き方はフォルダ(プロジェクト)ごとに1行なので、その名前がそのまま出どころ。
135
+ return withBlockRepos(
136
+ narrative,
137
+ activity,
138
+ Object.keys(byProject).map((p) => (p === UNSET_PROJECT ? [] : [p])),
139
+ );
140
+ };
141
+
142
+ /**
143
+ * 行ごとの作業したフォルダ(本人 2026-10-17:CLUBS が出どころで業務を推す)。
144
+ *
145
+ * AI には各行に repos(ダイジェストの [ ] のプロジェクト名から選ぶ)を答えさせ、ここで
146
+ * 行から**外して** narrative.blockRepos(workBlocks と同じ順の配列の配列)に移す。
147
+ * docHash の正準形(workBlocks など)には入れない=前の版の日報と照合の値が変わらない。
148
+ * 集計のフォルダの一覧(aggregates.projects・もともと送っている)にある名前だけ残す。
149
+ */
150
+ export const withBlockRepos = (narrative, activity, fallbackRepos = []) => {
151
+ const allowed = new Set(
152
+ (activity?.aggregates?.projects ?? []).map((x) => String(x).trim().toLowerCase()),
153
+ );
154
+ const blockRepos = [];
155
+ const workBlocks = (narrative.workBlocks ?? []).map((wb, i) => {
156
+ const { repos, ...rest } = wb ?? {};
157
+ const raw = Array.isArray(repos) ? repos : (fallbackRepos[i] ?? []);
158
+ blockRepos.push(
159
+ [
160
+ ...new Set(
161
+ raw
162
+ .filter((x) => typeof x === "string")
163
+ .map((x) => x.trim())
164
+ .filter((x) => x && allowed.has(x.toLowerCase())),
165
+ ),
166
+ ].slice(0, 4),
167
+ );
168
+ return rest;
169
+ });
170
+ return {
171
+ ...narrative,
172
+ workBlocks,
173
+ ...(blockRepos.some((r) => r.length) ? { blockRepos } : {}),
174
+ };
139
175
  };
140
176
 
141
177
  /**
@@ -167,8 +203,11 @@ export const generateNarrative = async (activity, config) => {
167
203
  }
168
204
 
169
205
  const prompt = buildPrompt(activity, {
170
- carryover: config.carryover,
206
+ handedOff: config.handedOff,
171
207
  terms: config.terms,
208
+ cases: config.cases,
209
+ decided: config.decided,
210
+ routines: config.routines,
172
211
  });
173
212
 
174
213
  try {
@@ -178,7 +217,7 @@ export const generateNarrative = async (activity, config) => {
178
217
  console.warn(`[narrative] ${generator.id} の応答を解釈できず fallback に切り替えます。`);
179
218
  return fallbackNarrative(activity, `${generator.id} の応答を解釈できず`, generator.id);
180
219
  }
181
- return { ...parsed, generator: generator.label(config) };
220
+ return withBlockRepos({ ...parsed, generator: generator.label(config) }, activity);
182
221
  } catch (e) {
183
222
  // 未インストール(ENOENT)は「その CLI がこの端末に無い」=設定ミスの典型なので明示する。
184
223
  const why =