@paxnax/berliner 0.3.0 → 0.4.0
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/README.md +2 -1
- package/dist/args.js +1 -0
- package/dist/commands/setup.js +11 -6
- package/dist/commands/upgrade.js +116 -0
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ With Bun, run `bunx --bun @paxnax/berliner@latest setup`. To install only the co
|
|
|
21
21
|
npx @paxnax/berliner@latest setup
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
It shows what it will do and asks once, then opens your browser so you can approve the connection. After you approve, it saves the token, installs the CLI globally, and installs the agent skill and a reporting hook for each of Claude Code, Codex and OpenCode that it finds. `--yes` skips the question, `--no-install` skips the global install, and `--no-browser` only prints the URL (over SSH, for example). Run `berliner
|
|
24
|
+
It shows what it will do and asks once, then opens your browser so you can approve the connection. After you approve, it saves the token, installs the CLI globally, and installs the agent skill and a reporting hook for each of Claude Code, Codex and OpenCode that it finds. `--yes` skips the question, `--no-install` skips the global install, and `--no-browser` only prints the URL (over SSH, for example). Run `berliner upgrade` to update the CLI and the skill to the latest release: it shows its plan and asks first, and a second question (or `--no-skill`) keeps your skill as it is.
|
|
25
25
|
|
|
26
26
|
3. From then on, the hooks report each session's cost, model, repository, commits and start and end times after every reply. Your agent adds the task, pull requests, coverage and evidence with `berliner report`, as the skill tells it to. Codex runs a new hook only once you trust it: open `codex` once in a terminal and trust the Berliner hook.
|
|
27
27
|
4. On **Settings**, list the subscriptions you pay for. Overview then compares each one with the sessions that ran on it.
|
|
@@ -118,6 +118,7 @@ Other commands:
|
|
|
118
118
|
- `berliner delete <session-id>`: delete a session, or one item with `--evidence <id>`. Asks to confirm unless `--yes`.
|
|
119
119
|
- `berliner evidence public <id>`: let anyone with the link view one file, and print its public URL and Markdown line. `berliner evidence private <id>` makes it private again. `berliner session` shows the ids, and the URL of each public file.
|
|
120
120
|
- `berliner skill`: print the agent skill (`SKILL.md`) to install in your agent.
|
|
121
|
+
- `berliner upgrade [--yes] [--no-skill]`: install the latest CLI and skill. It shows its plan (the versions, the exact install command and each skill file) and asks before changing anything; a second question, or `--no-skill`, leaves your skill as it is. Your token, config and hooks are never touched.
|
|
121
122
|
- `berliner hook <tool>`: run by the hooks setup installs, with the tool's payload on stdin. It never prints, never fails and never makes the agent wait; errors go to `${XDG_STATE_HOME:-~/.local/state}/berliner/hook.log`.
|
|
122
123
|
|
|
123
124
|
`report` and `update` exit 0 even when something fails, printing a warning that starts with `berliner:` to stderr, so reporting never breaks an agent's pipeline. Pass `--strict` to make them exit 1 on failure. The other commands exit 1 on failure.
|
package/dist/args.js
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -10,17 +10,23 @@ import { describeClaudeHooks, describeCodexHooks, describeOpenCodePlugin, instal
|
|
|
10
10
|
import { claudeDir, codexDir, detectTools, onPath, opencodeDir, skillTargets } from "../setup/targets.js";
|
|
11
11
|
// The same command the web guide shows.
|
|
12
12
|
const TEST_REPORT = 'berliner report --session-id try-1 --title "First report" --repo you/app --cost 0.10 --model test --plan pro';
|
|
13
|
-
const tilde = (p) => (p.startsWith(`${homedir()}/`) ? `~${p.slice(homedir().length)}` : p);
|
|
14
|
-
export async function confirm(input, output) {
|
|
13
|
+
export const tilde = (p) => (p.startsWith(`${homedir()}/`) ? `~${p.slice(homedir().length)}` : p);
|
|
14
|
+
export async function confirm(input, output, question = "Continue? [Y/n] ") {
|
|
15
15
|
const rl = createInterface({ input, output });
|
|
16
16
|
// Ctrl+C and Ctrl+D reject the question without ending the line; both mean no.
|
|
17
|
-
const answer = await rl.question(
|
|
17
|
+
const answer = await rl.question(question).catch(() => {
|
|
18
18
|
output.write("\n");
|
|
19
19
|
return "n";
|
|
20
20
|
});
|
|
21
21
|
rl.close();
|
|
22
22
|
return /^(y|yes)?$/i.test(answer.trim());
|
|
23
23
|
}
|
|
24
|
+
// Pinned to one version, so the command shown is the one run.
|
|
25
|
+
export const installCommand = (version) => [...(process.versions.bun ? ["bun", "add", "-g"] : ["npm", "install", "-g"]), `@paxnax/berliner@${version}`];
|
|
26
|
+
export function showPlan(title, steps) {
|
|
27
|
+
const plan = steps.flatMap((s, i) => [` ${i + 1}. ${s.label}`, ...s.detail.map((d) => ` ${d}`)]);
|
|
28
|
+
console.log(["", title, "", "This will:", ...plan, ""].join("\n"));
|
|
29
|
+
}
|
|
24
30
|
// The login of the saved token, or null when there is none or the server no longer accepts it.
|
|
25
31
|
async function savedLogin(url, token) {
|
|
26
32
|
if (!token)
|
|
@@ -47,7 +53,7 @@ export async function setup(env, values) {
|
|
|
47
53
|
};
|
|
48
54
|
const hooks = tools.filter((t) => t.found).map((t) => ({ ...t, ...hookFiles[t.id] }));
|
|
49
55
|
const version = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
50
|
-
const install =
|
|
56
|
+
const install = installCommand(version);
|
|
51
57
|
// The plan is data, built and shown before anything changes.
|
|
52
58
|
const steps = [
|
|
53
59
|
...(login
|
|
@@ -78,8 +84,7 @@ export async function setup(env, values) {
|
|
|
78
84
|
}),
|
|
79
85
|
},
|
|
80
86
|
];
|
|
81
|
-
|
|
82
|
-
console.log(["", "Berliner setup", "", "This will:", ...plan, ""].join("\n"));
|
|
87
|
+
showPlan("Berliner setup", steps);
|
|
83
88
|
if (!values.yes) {
|
|
84
89
|
if (!process.stdin.isTTY)
|
|
85
90
|
throw new CliError("There is no terminal to ask on. Run setup again with --yes to continue without asking.", "usage");
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { CliError } from "../client.js";
|
|
5
|
+
import { detectTools, skillTargets } from "../setup/targets.js";
|
|
6
|
+
import { confirm, installCommand, showPlan, tilde } from "./setup.js";
|
|
7
|
+
async function latestVersion(env) {
|
|
8
|
+
const registry = (env.npm_config_registry || "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(`${registry}/@paxnax/berliner/latest`);
|
|
11
|
+
if (!res.ok)
|
|
12
|
+
throw new Error(`HTTP ${res.status}`);
|
|
13
|
+
const { version } = (await res.json());
|
|
14
|
+
if (typeof version !== "string")
|
|
15
|
+
throw new Error("no version in the reply");
|
|
16
|
+
return version;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
throw new CliError(`Could not look up the latest berliner on ${registry} (${err.message}). Nothing was changed.`, "network");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// The skill text comes from the release being installed, so the plan's "already up to date" and what is written match it.
|
|
23
|
+
function skillOf(version, bundled) {
|
|
24
|
+
if (bundled)
|
|
25
|
+
return readFileSync(new URL("../../skill/SKILL.md", import.meta.url), "utf8");
|
|
26
|
+
// --bun keeps the CLI on Bun: its shebang prefers node when both exist.
|
|
27
|
+
const [cmd, ...pre] = process.versions.bun ? ["bunx", "--bun"] : ["npx", "--yes"];
|
|
28
|
+
const run = spawnSync(cmd, [...pre, `@paxnax/berliner@${version}`, "skill"], { encoding: "utf8" });
|
|
29
|
+
if (run.status !== 0 || !run.stdout)
|
|
30
|
+
throw new CliError(`Could not read the skill of berliner ${version} (${(run.error?.message ?? run.stderr).trim() || `exited with ${run.status}`}). Nothing was changed.`, "network");
|
|
31
|
+
return run.stdout;
|
|
32
|
+
}
|
|
33
|
+
// Release versions only (x.y.z): the registry's latest tag never points at a prerelease.
|
|
34
|
+
const newer = (a, b) => {
|
|
35
|
+
const [x, y] = [a, b].map((v) => v.split("-")[0].split(".").map(Number));
|
|
36
|
+
const i = x.findIndex((n, j) => n !== y[j]);
|
|
37
|
+
return i >= 0 && x[i] > y[i];
|
|
38
|
+
};
|
|
39
|
+
const sameAs = (path, text) => {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(path, "utf8") === text;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
// Only the command and the skill: the token, config and hooks are setup's, and stay as they are.
|
|
48
|
+
export async function upgrade(env, values) {
|
|
49
|
+
const current = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
50
|
+
const latest = await latestVersion(env);
|
|
51
|
+
// A CLI ahead of the registry (a local build, a release still propagating) is never taken backwards, nor is its skill.
|
|
52
|
+
const installing = newer(latest, current);
|
|
53
|
+
const target = installing ? latest : current;
|
|
54
|
+
const install = installCommand(latest);
|
|
55
|
+
const tools = detectTools(env);
|
|
56
|
+
const targets = values["no-skill"] ? [] : skillTargets(tools, env);
|
|
57
|
+
const skill = targets.length ? skillOf(target, !installing) : "";
|
|
58
|
+
let stale = targets.filter((t) => !sameAs(t.path, skill));
|
|
59
|
+
if (!installing && !stale.length) {
|
|
60
|
+
console.log(`Already up to date. berliner ${current} is ${current === latest ? "the latest release" : `newer than the latest release (${latest})`}.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
showPlan("Berliner upgrade", [
|
|
64
|
+
{
|
|
65
|
+
label: installing
|
|
66
|
+
? `Upgrade the berliner command from ${current} to ${latest}: ${install.join(" ")}`
|
|
67
|
+
: current === latest
|
|
68
|
+
? `berliner ${current} is already the latest, nothing to install`
|
|
69
|
+
: `berliner ${current} is newer than the latest release (${latest}), nothing to install`,
|
|
70
|
+
detail: [],
|
|
71
|
+
},
|
|
72
|
+
values["no-skill"]
|
|
73
|
+
? { label: "Leave the Berliner skill as it is (--no-skill)", detail: [] }
|
|
74
|
+
: {
|
|
75
|
+
label: `Upgrade the Berliner skill to ${target} for:`,
|
|
76
|
+
detail: tools.map((tool) => {
|
|
77
|
+
const target = targets.find((t) => t.tool === tool.id);
|
|
78
|
+
const where = target ? `${tilde(target.path)}${stale.includes(target) ? "" : " (already up to date)"}` : "not found, skipped";
|
|
79
|
+
return `${tool.name.padEnd(14)}${where}`;
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
if (!values.yes) {
|
|
84
|
+
if (!process.stdin.isTTY)
|
|
85
|
+
throw new CliError("There is no terminal to ask on. Run upgrade again with --yes to continue without asking.", "usage");
|
|
86
|
+
if (!(await confirm(process.stdin, process.stdout)))
|
|
87
|
+
throw new CliError("Cancelled. Nothing was changed.", "usage");
|
|
88
|
+
// Without an install, the skill is all Continue covered.
|
|
89
|
+
if (installing && stale.length && !(await confirm(process.stdin, process.stdout, "Upgrade the skill too? [Y/n] ")))
|
|
90
|
+
stale = [];
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
if (installing) {
|
|
94
|
+
const run = spawnSync(install[0], install.slice(1), { encoding: "utf8" });
|
|
95
|
+
if (run.status !== 0) {
|
|
96
|
+
console.error(`✗ Could not install the berliner command:\n${(run.error?.message ?? run.stdout + run.stderr).trimEnd()}`);
|
|
97
|
+
throw new CliError(`The skill was left as it is. Install the command yourself, then run upgrade again: ${install.join(" ")}`, "usage");
|
|
98
|
+
}
|
|
99
|
+
console.log(`✓ Installed berliner ${latest}`);
|
|
100
|
+
}
|
|
101
|
+
// A file that can't be written (read-only, say) is reported and skipped, so the other tools still get theirs.
|
|
102
|
+
let unwritten = false;
|
|
103
|
+
for (const t of stale) {
|
|
104
|
+
const what = `${tools.find((tool) => tool.id === t.tool).name} skill`;
|
|
105
|
+
try {
|
|
106
|
+
mkdirSync(dirname(t.path), { recursive: true });
|
|
107
|
+
writeFileSync(t.path, skill);
|
|
108
|
+
console.log(`✓ ${what}: ${tilde(t.path)}`);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
unwritten = true;
|
|
112
|
+
console.error(`✗ ${what}: ${tilde(t.path)} can't be written (${err.message.split(",")[0]})`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
console.log(unwritten ? "\nSome files can't be written. Fix the ones marked ✗ above, then run upgrade again." : "\nDone.");
|
|
116
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { setup } from "./commands/setup.js";
|
|
|
12
12
|
import { skill } from "./commands/skill.js";
|
|
13
13
|
import { summary } from "./commands/summary.js";
|
|
14
14
|
import { update } from "./commands/update.js";
|
|
15
|
+
import { upgrade } from "./commands/upgrade.js";
|
|
15
16
|
import { readConfig } from "./config.js";
|
|
16
17
|
import { warn } from "./output.js";
|
|
17
18
|
const usage = `Usage: berliner <command> [flags]
|
|
@@ -30,6 +31,7 @@ Commands:
|
|
|
30
31
|
delete <session-id> [--evidence <id>] [--yes]
|
|
31
32
|
evidence public <id> let anyone with the link view one file; prints its URL and Markdown [--json]
|
|
32
33
|
evidence private <id> make it viewable only by you again [--json]
|
|
34
|
+
upgrade install the latest CLI and skill [--yes --no-skill]
|
|
33
35
|
skill print the bundled SKILL.md
|
|
34
36
|
hook <tool> run by the hooks of claude-code, codex or opencode; reads the payload on stdin
|
|
35
37
|
|
|
@@ -37,7 +39,7 @@ Environment: BERLINER_TOKEN, BERLINER_URL, BERLINER_PLAN (replaces --plan)`;
|
|
|
37
39
|
const args = parse(process.argv.slice(2));
|
|
38
40
|
// Reporting must never fail the agent's own pipeline unless it asks to (anti-goal 1).
|
|
39
41
|
const softFail = (args.command === "report" || args.command === "update") && !args.values.strict;
|
|
40
|
-
const handled = new Set(["setup", "report", "update", "sessions", "session", "summary", "delete", "evidence", "skill", "hook"]);
|
|
42
|
+
const handled = new Set(["setup", "report", "update", "sessions", "session", "summary", "delete", "evidence", "skill", "hook", "upgrade"]);
|
|
41
43
|
const cfg = () => readConfig(process.env);
|
|
42
44
|
try {
|
|
43
45
|
if (args.values.help)
|
|
@@ -65,6 +67,8 @@ try {
|
|
|
65
67
|
await evidence(cfg(), args.values, args.positionals);
|
|
66
68
|
else if (args.command === "skill")
|
|
67
69
|
skill();
|
|
70
|
+
else if (args.command === "upgrade")
|
|
71
|
+
await upgrade(process.env, args.values);
|
|
68
72
|
else
|
|
69
73
|
throw new CliError(args.command ? `unknown command "${args.command}"` : "missing command", "usage");
|
|
70
74
|
}
|