@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.
- package/PROVENANCE.json +53 -47
- package/README.md +20 -11
- package/bin/clubs-nippo +0 -5
- package/bin/clubs-nippo-web.mjs +0 -5
- package/bin/clubs-nippo.mjs +1 -526
- package/bin/clubs.mjs +643 -0
- package/docs/events-schema.md +1 -1
- package/package.json +6 -5
- package/src/aggregate.mjs +32 -5
- package/src/aggregate.slots.test.mjs +23 -0
- package/src/autorun.mjs +1 -6
- package/src/autorun.test.mjs +2 -7
- package/src/cards.mjs +0 -5
- package/src/collect.mjs +0 -5
- package/src/config.mjs +10 -6
- package/src/generators/claude.mjs +0 -5
- package/src/generators/cli.mjs +0 -5
- package/src/generators/codex.mjs +0 -5
- package/src/generators/copilot.mjs +0 -5
- package/src/generators/gemini.mjs +0 -5
- package/src/generators/openai-compat.mjs +0 -5
- package/src/generators/openrouter.mjs +0 -5
- package/src/history.mjs +2 -5
- package/src/ingest.mjs +0 -5
- package/src/lemma.docHash.test.mjs +0 -5
- package/src/lemma.mjs +0 -5
- package/src/mask.mjs +0 -5
- package/src/mcp.mjs +26 -9
- package/src/narrative.blockRepos.test.mjs +48 -0
- package/src/narrative.mjs +47 -8
- package/src/profiles.mjs +121 -0
- package/src/profiles.test.mjs +73 -0
- package/src/prompt.mjs +99 -24
- package/src/prompt.test.mjs +87 -20
- package/src/remote-config.mjs +5 -6
- package/src/render.mjs +1 -5
- package/src/routines.mjs +61 -0
- package/src/routines.test.mjs +29 -0
- package/src/send.mjs +0 -5
- package/src/settings.mjs +0 -5
- package/src/sources/claude-code.mjs +0 -5
- package/src/sources/codex-cli.mjs +0 -5
- package/src/sources/copilot-cli.mjs +0 -5
- package/src/sources/cowork.mjs +0 -5
- package/src/sources/events-file.mjs +0 -5
- package/src/sources/gemini-cli.mjs +0 -5
- package/src/sources/hermes.mjs +0 -5
- package/src/store.mjs +0 -5
- package/src/tasks.mjs +0 -5
- package/src/tool-name.mjs +0 -5
- package/web/assets/qr.js +0 -5
- package/web/assets/sidebar.js +0 -5
- 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.
|
|
4
|
-
"description": "AI
|
|
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
|
|
35
|
-
"dry": "node ./bin/clubs
|
|
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
|
|
18
|
+
"@clubs.place/cli@latest",
|
|
24
19
|
"tick",
|
|
25
20
|
];
|
package/src/autorun.test.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 { 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
|
|
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
|
|
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)
|
|
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();
|
package/src/generators/cli.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 { execFile } from "node:child_process";
|
|
7
2
|
import { promisify } from "node:util";
|
|
8
3
|
import { ROOT } from "../config.mjs";
|
package/src/generators/codex.mjs
CHANGED
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
|
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
package/src/mask.mjs
CHANGED
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|