@clubs.place/cli 0.1.25 → 0.1.26
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/.env.example +8 -2
- package/PROVENANCE.json +28 -13
- package/bin/clubs.mjs +196 -16
- package/package.json +1 -1
- package/src/config.mjs +34 -11
- package/src/config.test.mjs +83 -0
- package/src/generator-env.mjs +42 -0
- package/src/generator-env.test.mjs +66 -0
- package/src/generators/hermes.mjs +188 -0
- package/src/generators/hermes.test.mjs +287 -0
- package/src/hermes-cron-script.test.mjs +105 -0
- package/src/hermes-cron.live.test.mjs +97 -0
- package/src/hermes-cron.mjs +421 -0
- package/src/hermes-cron.test.mjs +430 -0
- package/src/init.pipeline.test.mjs +130 -0
- package/src/narrative.hermes.test.mjs +74 -0
- package/src/narrative.mjs +30 -5
- package/src/narrative.targets.test.mjs +26 -0
- package/src/pack.test.mjs +23 -0
- package/src/profiles.mjs +25 -1
- package/src/profiles.test.mjs +15 -0
- package/src/prompt.mjs +1 -0
- package/src/prompt.test.mjs +5 -0
- package/src/sources/hermes.mjs +18 -9
- package/src/sources/hermes.test.mjs +129 -0
- package/src/tick.pipeline.test.mjs +166 -0
- package/src/tick.profiles.test.mjs +131 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, openSync, closeSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { buildTickScript, shQuote } from "./hermes-cron.mjs";
|
|
8
|
+
|
|
9
|
+
const BIN = new URL("../bin/clubs.mjs", import.meta.url).pathname;
|
|
10
|
+
const narrative = { workBlocks: [{ title: "test", text: "test", metric: "1" }], handoff: { title: "t", text: "t" }, insights: [], selfNote: "t" };
|
|
11
|
+
|
|
12
|
+
const run = (command, args, env) => new Promise((resolve, reject) => {
|
|
13
|
+
const output = join(env.HOME, "process-output.txt");
|
|
14
|
+
const fd = openSync(output, "w");
|
|
15
|
+
const child = spawn(command, args, { env, stdio: ["ignore", fd, fd] });
|
|
16
|
+
closeSync(fd);
|
|
17
|
+
const timer = setTimeout(() => { child.kill("SIGKILL"); }, 20_000);
|
|
18
|
+
child.on("error", reject);
|
|
19
|
+
child.on("close", (code) => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
resolve({ code, out: readFileSync(output, "utf8") });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 実 tick + fanout + collector + generator。通信だけを preload でスタブ化し、
|
|
26
|
+
// 認証・会話・report はすべて temp 配下の架空データに限定する。
|
|
27
|
+
const fixture = (t, { native = false, generator } = {}) => {
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), "nippo-profiles-"));
|
|
29
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
30
|
+
const home = join(dir, "clubs");
|
|
31
|
+
const childHome = join(home, "agents", "child");
|
|
32
|
+
const homes = [join(dir, "A"), join(dir, "B")];
|
|
33
|
+
for (const [i, h] of homes.entries()) {
|
|
34
|
+
mkdirSync(join(h, "sessions"), { recursive: true });
|
|
35
|
+
writeFileSync(join(h, "sessions", "session_20261028_120000.json"), JSON.stringify({
|
|
36
|
+
session_id: "s", source: "cli", session_start: "2026-10-28T12:00:00Z",
|
|
37
|
+
last_updated: "2026-10-28T12:00:00Z",
|
|
38
|
+
messages: [
|
|
39
|
+
{ role: "assistant", tool_calls: [{ id: "c1", function: { name: "terminal", arguments: JSON.stringify({ command: i ? "CHILD_ONLY_COMMAND" : "ROOT_ONLY_COMMAND" }) } }] },
|
|
40
|
+
{ role: "tool", tool_call_id: "c1", name: "terminal", content: '{"ok":true}' },
|
|
41
|
+
],
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
for (const [i, h] of [home, childHome].entries()) {
|
|
45
|
+
mkdirSync(h, { recursive: true });
|
|
46
|
+
writeFileSync(join(h, ".env"), [
|
|
47
|
+
"SOURCE=hermes", "TZ_OFFSET_MIN=0",
|
|
48
|
+
"NIPPO_INGEST_URL=https://clubs.invalid/api/nippo/ingest",
|
|
49
|
+
`NIPPO_INGEST_TOKEN=${i ? "child-token" : "root-token"}`,
|
|
50
|
+
// native root は wrapper の既定 A を使う。子の明示 B はそれに勝つ。
|
|
51
|
+
...(!native || i ? [`HERMES_HOME=${homes[i]}`] : []),
|
|
52
|
+
`OPENROUTER_API_KEY=${i ? "child-key" : "root-key"}`,
|
|
53
|
+
].join("\n") + "\n");
|
|
54
|
+
}
|
|
55
|
+
const log = join(dir, "calls.jsonl");
|
|
56
|
+
const preload = join(dir, "stub.mjs");
|
|
57
|
+
writeFileSync(preload, `
|
|
58
|
+
import { appendFileSync } from "node:fs";
|
|
59
|
+
globalThis.fetch = async (url, options = {}) => {
|
|
60
|
+
const record = { url: String(url), home: process.env.CLUBS_NIPPO_HOME, auth: options.headers?.authorization ?? options.headers?.Authorization, body: options.body };
|
|
61
|
+
appendFileSync(${JSON.stringify(log)}, JSON.stringify(record) + "\\n");
|
|
62
|
+
if (String(url).endsWith("/collector-config")) return new Response(JSON.stringify({
|
|
63
|
+
schedule: { enabled: true }, due: true, reportDate: "2026-10-28", sources: ["hermes"],
|
|
64
|
+
generator: process.env.CLUBS_NIPPO_CHILD && !${native} ? { backend: "openrouter", model: "child/model" } : { backend: "hermes-cli" }
|
|
65
|
+
}));
|
|
66
|
+
if (String(url).includes("openrouter")) return new Response(JSON.stringify({
|
|
67
|
+
choices: [{ message: { content: ${JSON.stringify(JSON.stringify(narrative))} } }]
|
|
68
|
+
}));
|
|
69
|
+
if (String(url).endsWith("/ingest")) return new Response("{}");
|
|
70
|
+
throw new Error("Unexpected request: " + url);
|
|
71
|
+
};
|
|
72
|
+
`);
|
|
73
|
+
const hermes = join(dir, "hermes");
|
|
74
|
+
writeFileSync(hermes, `#!/bin/sh
|
|
75
|
+
cat > "$CLUBS_NIPPO_HOME/prompt.txt"
|
|
76
|
+
printf '{"generator":"hermes","home":"%s","hermesHome":"%s","key":"%s"}\\n' "$CLUBS_NIPPO_HOME" "$HERMES_HOME" "$OPENROUTER_API_KEY" >> ${shQuote(log)}
|
|
77
|
+
printf '%s' ${shQuote(JSON.stringify(narrative))}
|
|
78
|
+
`, { mode: 0o755 });
|
|
79
|
+
const env = {
|
|
80
|
+
PATH: process.env.PATH, HOME: dir, CLUBS_NIPPO_HOME: home,
|
|
81
|
+
HERMES_BIN: hermes, NODE_OPTIONS: `--import=${preload}`,
|
|
82
|
+
...(generator ? { GENERATOR: generator } : {}),
|
|
83
|
+
};
|
|
84
|
+
return { dir, home, childHome, homes, env, log, calls: () => readFileSync(log, "utf8").trim().split("\n").map(JSON.parse) };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const explicit of [false, true]) {
|
|
88
|
+
test(`root+child tick: ${explicit ? "真の GENERATOR 上書きを継承" : "root の導出 GENERATOR より子の OpenRouter 設定が優先"}`, async (t) => {
|
|
89
|
+
const f = fixture(t, { generator: explicit ? "hermes" : undefined });
|
|
90
|
+
const r = await run(process.execPath, [BIN, "tick"], f.env);
|
|
91
|
+
assert.equal(r.code, 0, r.out);
|
|
92
|
+
const calls = f.calls();
|
|
93
|
+
const generators = calls.filter((c) => c.generator || c.url.includes("openrouter"));
|
|
94
|
+
assert.equal(generators.length, 2, r.out);
|
|
95
|
+
assert.equal(generators[0].hermesHome, f.homes[0]);
|
|
96
|
+
assert.equal(generators[1].home, f.childHome);
|
|
97
|
+
if (explicit) {
|
|
98
|
+
assert.equal(generators[1].generator, "hermes");
|
|
99
|
+
assert.equal(generators[1].hermesHome, f.homes[1]);
|
|
100
|
+
} else {
|
|
101
|
+
assert.match(r.out, /generator=openrouter/);
|
|
102
|
+
assert.equal(JSON.parse(generators[1].body).model, "child/model");
|
|
103
|
+
assert.equal(generators[1].auth, "Bearer child-key");
|
|
104
|
+
}
|
|
105
|
+
assert.equal(calls.filter((c) => c.url?.endsWith("/ingest")).length, 2);
|
|
106
|
+
const childCalls = JSON.stringify(calls.filter((c) => c.home === f.childHome));
|
|
107
|
+
assert.ok(!childCalls.includes("root-key"));
|
|
108
|
+
assert.ok(!childCalls.includes("root-token"));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test("native cron wrapper: 所有者 A と child.env B の collect + generation を分離", async (t) => {
|
|
113
|
+
const f = fixture(t, { native: true });
|
|
114
|
+
const script = join(f.dir, "tick.sh");
|
|
115
|
+
writeFileSync(script, buildTickScript({
|
|
116
|
+
home: f.home, hermesHome: f.homes[0],
|
|
117
|
+
tickCmd: `${shQuote(process.execPath)} ${shQuote(BIN)} tick --only=hermes`,
|
|
118
|
+
logPath: join(f.dir, "cron.log"),
|
|
119
|
+
}));
|
|
120
|
+
const r = await run("bash", [script], { ...f.env, HERMES_HOME: f.homes[0] });
|
|
121
|
+
assert.equal(r.code, 0, r.out);
|
|
122
|
+
const generated = f.calls().filter((c) => c.generator);
|
|
123
|
+
assert.equal(generated.length, 2, r.out);
|
|
124
|
+
for (const [i, c] of generated.entries()) {
|
|
125
|
+
assert.equal(c.hermesHome, f.homes[i]);
|
|
126
|
+
assert.equal(c.key, i ? "child-key" : "root-key");
|
|
127
|
+
const prompt = readFileSync(join(c.home, "prompt.txt"), "utf8");
|
|
128
|
+
assert.ok(prompt.includes(i ? "CHILD_ONLY_COMMAND" : "ROOT_ONLY_COMMAND"));
|
|
129
|
+
assert.ok(!prompt.includes(i ? "ROOT_ONLY_COMMAND" : "CHILD_ONLY_COMMAND"));
|
|
130
|
+
}
|
|
131
|
+
});
|