@clubs.place/cli 0.1.18 → 0.1.23
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 +54 -47
- package/README.md +11 -0
- 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 +0 -5
- package/src/autorun.test.mjs +0 -5
- 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 +0 -5
- package/src/narrative.blockRepos.test.mjs +48 -0
- package/src/narrative.mjs +47 -8
- package/src/profiles.mjs +110 -0
- package/src/profiles.test.mjs +66 -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.23",
|
|
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
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";
|
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
|
@@ -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 =
|
package/src/profiles.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 1台で複数の AI(つなぎ先)を動かす(本人 2026-10-09)。
|
|
7
|
+
*
|
|
8
|
+
* これまで設定は ~/.clubs-nippo/.env の1つだけで、Codex CLI をつなぐと
|
|
9
|
+
* Claude Code の設定(鍵・集める元)が上書きされ、Claude Code の日報が抜けた。
|
|
10
|
+
*
|
|
11
|
+
* - 最初のつなぎ先は今までどおり APP_HOME(~/.clubs-nippo/.env)
|
|
12
|
+
* - 別の AI をつなぐと、APP_HOME/agents/<AIのID>/.env に並べて置く
|
|
13
|
+
* (同じ AI の鍵の再発行・使えなくなった鍵は、今までどおり上書き)
|
|
14
|
+
* - 毎時の tick と手動の実行は、APP_HOME の分のあと、並べた分を1つずつ
|
|
15
|
+
* 子プロセスで動かす(CLUBS_NIPPO_HOME を替えて同じコマンドを走らせる)。
|
|
16
|
+
* 自動実行(launchd / cron)は1本のまま。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const PROFILE_DIR = "agents";
|
|
20
|
+
|
|
21
|
+
/** 子プロセスの目印(子は並べた分をさらに回さない)。 */
|
|
22
|
+
export const CHILD_FLAG = "CLUBS_NIPPO_CHILD";
|
|
23
|
+
|
|
24
|
+
export const profilesDir = (home) => join(home, PROFILE_DIR);
|
|
25
|
+
|
|
26
|
+
/** 並べた設定の置き場所(.env のあるものだけ・名前順)。 */
|
|
27
|
+
export const listProfiles = (home) => {
|
|
28
|
+
const dir = profilesDir(home);
|
|
29
|
+
if (!existsSync(dir)) return [];
|
|
30
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
31
|
+
.filter((e) => e.isDirectory() && existsSync(join(dir, e.name, ".env")))
|
|
32
|
+
.map((e) => join(dir, e.name))
|
|
33
|
+
.sort();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** .env を読む(コメント・空行は飛ばす)。無ければ空。 */
|
|
37
|
+
export const readEnvFile = (path) => {
|
|
38
|
+
if (!existsSync(path)) return {};
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
41
|
+
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
42
|
+
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** ディレクトリ名に使える形(AI の ID は ULID なのでそのまま通る想定)。 */
|
|
48
|
+
const safe = (s) => String(s).replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "agent";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* つなぎ直し(init)の書き先を決める。
|
|
52
|
+
*
|
|
53
|
+
* root … APP_HOME の .env({NIPPO_INGEST_TOKEN, AGENT_ID, …})
|
|
54
|
+
* rootIs … 今の鍵で CLUBS に聞いた結果:
|
|
55
|
+
* { status: 'ok', agentId } / { status: 'dead' }(401・403=もう使えない)
|
|
56
|
+
* / { status: 'unknown' }(届かない・古いサーバで ID が無い)
|
|
57
|
+
* newAgent … 引き換えた AI の ID(古いサーバでは無いこともある)
|
|
58
|
+
* newSource… 接続コマンドの --source(集める元)
|
|
59
|
+
*
|
|
60
|
+
* 上書きしてよいのは:まだ何もつないでいない・同じ AI・今の鍵がもう使えない、の
|
|
61
|
+
* どれか。集める元が今と同じときも上書き——同じ機械の同じ記録を2回読むだけなので
|
|
62
|
+
* 並べる意味が無い(ID を返さない古いサーバでの、同じ AI のつなぎ直しもここで拾う)。
|
|
63
|
+
* それ以外(別の AI・分からない)は並べて置く——分からないときに上書き
|
|
64
|
+
* すると、今動いている AI の日報が黙って止まる(10/8 の事故)。
|
|
65
|
+
*/
|
|
66
|
+
export const chooseInitHome = ({ home, root, rootIs, newAgent, newSource, profiles = [] }) => {
|
|
67
|
+
// すでに並べてある同じ AI があれば、そこを更新する。
|
|
68
|
+
const existing = newAgent
|
|
69
|
+
? profiles.find((p) => readEnvFile(join(p, ".env")).AGENT_ID === newAgent)
|
|
70
|
+
: undefined;
|
|
71
|
+
if (existing) return existing;
|
|
72
|
+
if (!root?.NIPPO_INGEST_TOKEN) return home;
|
|
73
|
+
const rootAgent = root.AGENT_ID || (rootIs?.status === "ok" ? rootIs.agentId : undefined);
|
|
74
|
+
if (newAgent && rootAgent && rootAgent === newAgent) return home;
|
|
75
|
+
if (rootIs?.status === "dead") return home;
|
|
76
|
+
if (newSource && root.SOURCE && root.SOURCE === newSource) return home;
|
|
77
|
+
return join(profilesDir(home), safe(newAgent ?? `agent-${Date.now()}`));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 子プロセスに渡す環境。APP_HOME の .env から読み込んだ値(鍵・集める元)は
|
|
82
|
+
* 落とす——残すと「実環境の値が勝つ」決まりで、並べた AI が APP_HOME の鍵で
|
|
83
|
+
* 動いてしまう。
|
|
84
|
+
*/
|
|
85
|
+
export const childEnv = (env, loadedKeys, home) => {
|
|
86
|
+
const out = { ...env };
|
|
87
|
+
for (const k of loadedKeys) delete out[k];
|
|
88
|
+
out.CLUBS_NIPPO_HOME = home;
|
|
89
|
+
out[CHILD_FLAG] = "1";
|
|
90
|
+
return out;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** 並べた AI を1つずつ動かす(同じ引数で・出力はそのまま流す)。 */
|
|
94
|
+
export const runProfiles = async ({ home, args, loadedKeys, script, node = process.execPath }) => {
|
|
95
|
+
for (const dir of listProfiles(home)) {
|
|
96
|
+
const name = readEnvFile(join(dir, ".env")).SOURCE || dir.split("/").pop();
|
|
97
|
+
console.log(`\n[agents] ${name} の分(${dir})`);
|
|
98
|
+
await new Promise((resolve) => {
|
|
99
|
+
const child = spawn(node, [script, ...args], {
|
|
100
|
+
env: childEnv(process.env, loadedKeys, dir),
|
|
101
|
+
stdio: "inherit",
|
|
102
|
+
});
|
|
103
|
+
child.on("exit", () => resolve());
|
|
104
|
+
child.on("error", (e) => {
|
|
105
|
+
console.log(`[agents] 動かせませんでした(${e?.message ?? e})`);
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { chooseInitHome, childEnv, listProfiles, CHILD_FLAG } from "./profiles.mjs";
|
|
7
|
+
|
|
8
|
+
const home = () => mkdtempSync(join(tmpdir(), "nippo-"));
|
|
9
|
+
|
|
10
|
+
test("まだ何もつないでいなければ APP_HOME に書く", () => {
|
|
11
|
+
const h = home();
|
|
12
|
+
assert.equal(chooseInitHome({ home: h, root: {}, newAgent: "A" }), h);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("別の AI をつなぐと上書きせず agents/<ID> に並べる(10/8 の事故)", () => {
|
|
16
|
+
const h = home();
|
|
17
|
+
const got = chooseInitHome({
|
|
18
|
+
home: h,
|
|
19
|
+
root: { NIPPO_INGEST_TOKEN: "t" },
|
|
20
|
+
rootIs: { status: "ok", agentId: "CODEX" },
|
|
21
|
+
newAgent: "CLAUDE",
|
|
22
|
+
});
|
|
23
|
+
assert.equal(got, join(h, "agents", "CLAUDE"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("同じ AI の鍵の再発行・使えなくなった鍵は APP_HOME を上書き", () => {
|
|
27
|
+
const h = home();
|
|
28
|
+
const root = { NIPPO_INGEST_TOKEN: "t" };
|
|
29
|
+
assert.equal(chooseInitHome({ home: h, root, rootIs: { status: "ok", agentId: "A" }, newAgent: "A" }), h);
|
|
30
|
+
assert.equal(chooseInitHome({ home: h, root: { ...root, AGENT_ID: "A" }, rootIs: { status: "unknown" }, newAgent: "A" }), h);
|
|
31
|
+
assert.equal(chooseInitHome({ home: h, root, rootIs: { status: "dead" }, newAgent: "B" }), h);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("確かめられないときは上書きしない(並べる)", () => {
|
|
35
|
+
const h = home();
|
|
36
|
+
const got = chooseInitHome({ home: h, root: { NIPPO_INGEST_TOKEN: "t" }, rootIs: { status: "unknown" }, newAgent: "B" });
|
|
37
|
+
assert.equal(got, join(h, "agents", "B"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("集める元が今と同じなら上書き(ID を返さない古いサーバでの同じ AI のつなぎ直し)", () => {
|
|
41
|
+
const h = home();
|
|
42
|
+
const root = { NIPPO_INGEST_TOKEN: "t", SOURCE: "claude-code" };
|
|
43
|
+
assert.equal(chooseInitHome({ home: h, root, rootIs: { status: "unknown" }, newAgent: "A", newSource: "claude-code" }), h);
|
|
44
|
+
assert.equal(
|
|
45
|
+
chooseInitHome({ home: h, root, rootIs: { status: "unknown" }, newAgent: "B", newSource: "codex-cli" }),
|
|
46
|
+
join(h, "agents", "B"),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("すでに並べてある同じ AI はそこを更新する", () => {
|
|
51
|
+
const h = home();
|
|
52
|
+
const dir = join(h, "agents", "X");
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
writeFileSync(join(dir, ".env"), "AGENT_ID=B\n");
|
|
55
|
+
assert.deepEqual(listProfiles(h), [dir]);
|
|
56
|
+
const got = chooseInitHome({ home: h, root: { NIPPO_INGEST_TOKEN: "t" }, rootIs: { status: "ok", agentId: "A" }, newAgent: "B", profiles: listProfiles(h) });
|
|
57
|
+
assert.equal(got, dir);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("子プロセスには APP_HOME の .env の値を渡さない", () => {
|
|
61
|
+
const env = childEnv({ NIPPO_INGEST_TOKEN: "root", PATH: "/bin" }, ["NIPPO_INGEST_TOKEN"], "/x");
|
|
62
|
+
assert.equal(env.NIPPO_INGEST_TOKEN, undefined);
|
|
63
|
+
assert.equal(env.PATH, "/bin");
|
|
64
|
+
assert.equal(env.CLUBS_NIPPO_HOME, "/x");
|
|
65
|
+
assert.equal(env[CHILD_FLAG], "1");
|
|
66
|
+
});
|