@paxnax/berliner 0.8.0 → 0.9.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 +22 -16
- package/dist/args.js +1 -0
- package/dist/commands/body.js +32 -21
- package/dist/commands/hook.js +15 -9
- package/dist/commands/report.js +6 -6
- package/dist/commands/summary.js +1 -1
- package/dist/commands/update.js +3 -3
- package/dist/config.js +5 -1
- package/dist/index.js +5 -3
- package/package.json +1 -1
- package/skill/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -36,26 +36,26 @@ Setup saves the token and, when it isn't the default, the server address in `${X
|
|
|
36
36
|
|---|---|---|
|
|
37
37
|
| `BERLINER_TOKEN` | only without setup | An API token, created on the Settings page at https://berliner.app. Without it, the CLI uses the token in the config file. |
|
|
38
38
|
| `BERLINER_URL` | no | The server to report to. Without it, the CLI uses the config file's `url`, then `https://berliner.app`; set it for a local or self-hosted server. |
|
|
39
|
-
| `
|
|
39
|
+
| `BERLINER_VENDOR_SUBSCRIPTION_SLUG` | no | The slug of the subscription the sessions from this shell run on, from Settings → "Third-party vendor subscriptions". A session is put on it when it is created, by `berliner report`, `berliner update` or the hooks, and its plan replaces whatever `--plan` the agent passes, so set it when one machine runs several accounts. A session that already exists never moves to it. Empty means unset. See [Several subscriptions](#several-subscriptions). |
|
|
40
40
|
|
|
41
41
|
## Several subscriptions
|
|
42
42
|
|
|
43
|
-
Berliner compares each subscription only with the sessions that ran on it. A session belongs to a subscription
|
|
43
|
+
Berliner compares each subscription only with the sessions that ran on it. Each subscription in Settings → "Third-party vendor subscriptions" has a slug, a short name of your choosing such as `work`. Leave it blank and it is `<vendor>-<plan>`, like `anthropic-max_x20` or `openai-pro`. A session belongs to a subscription:
|
|
44
44
|
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
45
|
+
- **When the shell names it** with `BERLINER_VENDOR_SUBSCRIPTION_SLUG` as the session is created, or a report or update passes `--subscription <slug>`, which wins over the variable. The session takes that subscription's plan, whatever `--plan` the agent passed. A session that already exists stays on its subscription unless `--subscription` moves it. The subscription must be for the model's vendor: `claude-…` is Claude, and `gpt-…`, `codex-…` or `o3`-style names are OpenAI.
|
|
46
|
+
- **Otherwise by its plan**: the agent's `--plan`, or, when there is none (the hooks never pass `--plan`, and reports inside Claude Code, Codex or OpenCode can leave it out), the plan of your one subscription for the model's vendor. The session belongs to your one subscription for that vendor and plan, if there is exactly one. The plan is `api` when you list no subscription for that vendor, more than one, or the model is from neither vendor.
|
|
47
47
|
|
|
48
|
-
An agent usually can't tell which account it's signed in to, so its `--plan` is a guess. If you run a work and a personal profile on one machine,
|
|
48
|
+
An agent usually can't tell which account it's signed in to, so its `--plan` is a guess. If you run a work and a personal profile on one machine, name each one's subscription with `BERLINER_VENDOR_SUBSCRIPTION_SLUG`, one alias per profile. The hooks run inside the tool, so they see it too:
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
|
-
alias claude='
|
|
52
|
-
alias pclaude='CLAUDE_CONFIG_DIR=$HOME/.claude-personal
|
|
53
|
-
alias codex='
|
|
51
|
+
alias claude='BERLINER_VENDOR_SUBSCRIPTION_SLUG=work command claude'
|
|
52
|
+
alias pclaude='CLAUDE_CONFIG_DIR=$HOME/.claude-personal BERLINER_VENDOR_SUBSCRIPTION_SLUG=personal command claude'
|
|
53
|
+
alias codex='BERLINER_VENDOR_SUBSCRIPTION_SLUG=openai-pro command codex'
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Write `command claude`, not `claude`, inside each alias. Without `command`, `pclaude` expands the `claude` alias too, and its `
|
|
56
|
+
Write `command claude`, not `claude`, inside each alias. Without `command`, `pclaude` expands the `claude` alias too, and its `BERLINER_VENDOR_SUBSCRIPTION_SLUG=work` wins. Two subscriptions on the same plan work too, as long as their slugs differ. If you rename a slug in Settings, update the aliases that use it.
|
|
57
57
|
|
|
58
|
-
When
|
|
58
|
+
When the variable is set and the agent passes a different `--plan`, Berliner uses the subscription's plan and prints a warning. If the variable's slug isn't one of your subscriptions, or is for the other vendor, the session is saved with the agent's `--plan` instead, with a warning. With no `--plan`, as in the hooks, the report is sent without a plan instead, so a new session gets the server's pick and an existing one keeps its plan: `berliner report` prints a warning, and the hook writes one line to `${XDG_STATE_HOME:-~/.local/state}/berliner/hook.log`. Either way a typo never loses a session. A bad `--subscription` is different: you typed it, so the report fails with the server's message and nothing is saved.
|
|
59
59
|
|
|
60
60
|
The plan values:
|
|
61
61
|
|
|
@@ -77,10 +77,16 @@ The plan values:
|
|
|
77
77
|
| OpenAI | Business (premium seat) | `business_premium` |
|
|
78
78
|
| either | Billed per token | `api` |
|
|
79
79
|
|
|
80
|
-
Sessions reported before you set
|
|
80
|
+
Sessions reported before you set the variable keep the plan the agent guessed. To move one to a subscription by hand:
|
|
81
81
|
|
|
82
82
|
```sh
|
|
83
|
-
|
|
83
|
+
berliner update 3f1c2a9e-7b64-4d0a-9a51-2c8f0e6d1b77 --subscription work
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
While the variable is set, `report` and `update` leave `--plan` out when they change a session that already exists, with a warning, so a guessed plan can't move it off your subscription. To set a plan by hand from such a shell, clear the variable for that command:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
BERLINER_VENDOR_SUBSCRIPTION_SLUG= berliner update 3f1c2a9e-7b64-4d0a-9a51-2c8f0e6d1b77 --plan pro
|
|
84
90
|
```
|
|
85
91
|
|
|
86
92
|
## Commands
|
|
@@ -102,19 +108,19 @@ berliner report \
|
|
|
102
108
|
--evidence "./shots/after.png::Login page after the fix"
|
|
103
109
|
```
|
|
104
110
|
|
|
105
|
-
Reporting again with the same `--session-id` replaces the values, adds PR URLs and skips evidence already uploaded. `--plan` is required unless `
|
|
111
|
+
Reporting again with the same `--session-id` replaces the values, adds PR URLs and skips evidence already uploaded. `--plan` is required unless you pass `--subscription <slug>` or `BERLINER_VENDOR_SUBSCRIPTION_SLUG` is set.
|
|
106
112
|
|
|
107
113
|
Evidence is private: only you can see it, signed in. Add `--public` to also make each `--evidence` file, new or already uploaded, viewable by anyone with its link. The report then prints one line per file to paste into a pull request body: a Markdown image (``), or the bare URL for a video, which GitHub does not embed from other hosts. With `--json` they are in `evidence.published`. Browsers and GitHub may keep a copy of a public file for up to 5 minutes after you make it private again.
|
|
108
114
|
|
|
109
|
-
Inside Claude Code, Codex or OpenCode, leave out `--session-id`, `--repo`, `--cost`, `--model` and `--plan`. The CLI fills in the session id, cost and model from the tool's own records and the repository from git, and ignores an explicit `--session-id`, `--cost` or `--model` with one warning. The plan comes from `
|
|
115
|
+
Inside Claude Code, Codex or OpenCode, leave out `--session-id`, `--repo`, `--cost`, `--model` and `--plan`. The CLI fills in the session id, cost and model from the tool's own records and the repository from git, and ignores an explicit `--session-id`, `--cost` or `--model` with one warning. The plan comes from `BERLINER_VENDOR_SUBSCRIPTION_SLUG` or your subscriptions, as above. A report there without `--title` keeps the existing title; a new session takes the first line of your first prompt, cut to 80 characters.
|
|
110
116
|
|
|
111
117
|
Other commands:
|
|
112
118
|
|
|
113
119
|
- `berliner setup [--yes] [--no-install] [--no-browser]`: connect this terminal and install the CLI, the skill and the hooks, as in [Quickstart](#quickstart).
|
|
114
|
-
- `berliner update <session-id> [flags]`: change fields of a session without resending everything. The plan changes only when you pass `--plan
|
|
120
|
+
- `berliner update <session-id> [flags]`: change fields of a session without resending everything. The plan changes only when you pass `--plan` (left out while `BERLINER_VENDOR_SUBSCRIPTION_SLUG` is set, see [Several subscriptions](#several-subscriptions)), or `--subscription <slug>`, which moves the session to that subscription and its plan; `BERLINER_VENDOR_SUBSCRIPTION_SLUG` only applies when update creates the session, so an old session never moves to the shell's subscription. Inside the three tools, leave out the session id.
|
|
115
121
|
- `berliner sessions [filters]`: list sessions. Filters: `--repo --model --project --ci --merge --since <N>d` or `--from --to`, plus `--limit` and `--json`.
|
|
116
122
|
- `berliner session <session-id>`: show one session.
|
|
117
|
-
- `berliner summary [filters]`: totals for the same filters as `sessions`, then one line per subscription
|
|
123
|
+
- `berliner summary [filters]`: totals for the same filters as `sessions`, then one line per subscription, labelled with its slug: `Claude Max x20 (work)`.
|
|
118
124
|
- `berliner projects [--json]`: the project names in use, then each repository's default project.
|
|
119
125
|
- `berliner project [<name>] [--none] [--repo owner/name] [--json]`: with no name, show this repository's default project; with a name, save it; `--none` saves that the repository has no project. The repository comes from `git remote get-url origin` unless you pass `--repo`. A session's project is its own `--project`, else its repository's default, else its ticket's prefix (`SMTNC-3268` gives `SMTNC`). Until a repository has a default, `report` and `update` print a `Project: none saved` line, which asks your agent to suggest one to you.
|
|
120
126
|
- `berliner team [<uuid>] [--none]`: with no argument, print the team your sessions report to and where it came from; with a uuid, save it for this machine; `--none` clears it. A session reports to a team, or to your own plan, from the first of these that is set: `--team <uuid>` or `--no-team` on `report` and `update`, `team` in `.berliner.json` at the repository root (`{ "team": "<uuid>" }`, meant to be committed so the whole team shares it), `BERLINER_TEAM`, then the saved setting. `--no-team` beats all of them. Only the report that creates a session sends the team; it stays fixed after that, and a later setting that differs prints a warning. The server refuses a team report with `team_not_found`, `team_not_member`, `team_not_eligible` (the repository's owner is not an organization the team covers) or `team_inactive` (the team has no active plan), and the message says how to fix it. Inside Claude Code, Codex or OpenCode the hook cannot print, so the next `berliner report`, `update`, `session` or `sessions` in that repository prints it once. The team's owner sees every session and file reported to the team; members never see each other's.
|
package/dist/args.js
CHANGED
package/dist/commands/body.js
CHANGED
|
@@ -21,6 +21,7 @@ export function sessionBody(values) {
|
|
|
21
21
|
cost_status: values["cost-status"],
|
|
22
22
|
model: values.model,
|
|
23
23
|
plan: values.plan,
|
|
24
|
+
subscription: values.subscription,
|
|
24
25
|
task_id: values.task,
|
|
25
26
|
task_url: values["task-url"],
|
|
26
27
|
project: values.project,
|
|
@@ -58,11 +59,11 @@ export async function fromTool(tool, values, env, command) {
|
|
|
58
59
|
return filled;
|
|
59
60
|
}
|
|
60
61
|
// Inside a tool: the same fields, minus a derived title, for a session that exists; on a 404 (the first turn, before the hook
|
|
61
|
-
// has reported), the PUT that creates it, at cost 0 while the cost is unknown
|
|
62
|
-
//
|
|
63
|
-
export async function patchFirst(cfg, path, body, titled,
|
|
62
|
+
// has reported), the PUT that creates it, at cost 0 while the cost is unknown. Create-only: when another report created the
|
|
63
|
+
// session since the 404 (412), the PATCH after all, so its title and cost stay.
|
|
64
|
+
export async function patchFirst(cfg, path, body, titled, team = null) {
|
|
64
65
|
const patch = async () => {
|
|
65
|
-
const res = await saveSession(cfg, "PATCH", path, titled ? body : { ...body, title: undefined }
|
|
66
|
+
const res = await saveSession(cfg, "PATCH", path, titled ? body : { ...body, title: undefined });
|
|
66
67
|
return { ...res, json: { created: false, ...res.json } };
|
|
67
68
|
};
|
|
68
69
|
try {
|
|
@@ -71,8 +72,8 @@ export async function patchFirst(cfg, path, body, titled, guess, team = null) {
|
|
|
71
72
|
catch (err) {
|
|
72
73
|
if (!(err instanceof CliError && err.status === 404))
|
|
73
74
|
throw err;
|
|
74
|
-
const create = { ...body, cost_usd: body.cost_usd ?? 0,
|
|
75
|
-
return saveSession(cfg, "PUT", path, create,
|
|
75
|
+
const create = { ...body, cost_usd: body.cost_usd ?? 0, team: team ?? undefined };
|
|
76
|
+
return saveSession(cfg, "PUT", path, create, { "If-None-Match": "*" }).catch((err) => {
|
|
76
77
|
if (err instanceof CliError && err.status === 412)
|
|
77
78
|
return patch();
|
|
78
79
|
throw err;
|
|
@@ -141,20 +142,27 @@ function prefilterPrs(body) {
|
|
|
141
142
|
return { body: { ...body, prs: kept.length ? kept : undefined }, dropped };
|
|
142
143
|
}
|
|
143
144
|
// R46/R49: one invalid optional value must not lose the session. Bad PRs are filtered first; on a 422 naming
|
|
144
|
-
// only optional fields, drop them and retry once.
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
145
|
+
// only optional fields, drop them and retry once. Anything else is thrown as before. Warnings are printed only once the
|
|
146
|
+
// save succeeded, so a failed save never claims fields were dropped.
|
|
147
|
+
// A PUT (the report that creates a session) without --subscription is on BERLINER_VENDOR_SUBSCRIPTION_SLUG; a PATCH never
|
|
148
|
+
// is, so an existing session never moves to this shell's subscription. Nor does such a PATCH send --plan: the server would
|
|
149
|
+
// re-link the session by plan, off the subscription the variable gave it. A slug from the variable that the server rejects
|
|
150
|
+
// is left out of the retry, which keeps the agent's --plan; without one, a new session gets the server's pick. An explicit
|
|
151
|
+
// --subscription is never dropped.
|
|
152
|
+
export async function saveSession(cfg, method, path, body, headers) {
|
|
153
|
+
const aliased = body.subscription === undefined && cfg.subscription !== null;
|
|
154
|
+
const fromEnv = aliased && method === "PUT";
|
|
155
|
+
const unplanned = aliased && method === "PATCH" && body.plan !== undefined;
|
|
156
|
+
const input = fromEnv ? { ...body, subscription: cfg.subscription ?? undefined } : unplanned ? { ...body, plan: undefined } : body;
|
|
149
157
|
const first = prefilterPrs(input);
|
|
150
158
|
const dropped = first.dropped;
|
|
151
|
-
|
|
152
|
-
let fellBack = false;
|
|
159
|
+
let rejected;
|
|
153
160
|
const finish = (res) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
const guess = body.plan;
|
|
162
|
+
if (rejected !== undefined)
|
|
163
|
+
warn(`BERLINER_VENDOR_SUBSCRIPTION_SLUG "${cfg.subscription}" isn't used: ${rejected}; ${guess === undefined ? "sent the report without a plan" : `used --plan ${guess}`}`);
|
|
164
|
+
else if (unplanned || (fromEnv && guess !== undefined && res.json.session.plan !== guess))
|
|
165
|
+
warn(`--plan ${guess} ignored, BERLINER_VENDOR_SUBSCRIPTION_SLUG is ${cfg.subscription}`);
|
|
158
166
|
for (const d of dropped)
|
|
159
167
|
warn(`${d.field}: ${d.reason}`);
|
|
160
168
|
return { ...res, dropped };
|
|
@@ -164,11 +172,14 @@ export async function saveSession(cfg, method, path, input, guess, headers) {
|
|
|
164
172
|
}
|
|
165
173
|
catch (err) {
|
|
166
174
|
const fieldErrors = err instanceof CliError && err.status === 422 ? err.details?.fieldErrors : undefined;
|
|
167
|
-
const names = Object.keys(fieldErrors ?? {}).filter((n) => !(n === "
|
|
168
|
-
|
|
169
|
-
|
|
175
|
+
const names = Object.keys(fieldErrors ?? {}).filter((n) => !(n === "subscription" && fromEnv));
|
|
176
|
+
if (fromEnv && fieldErrors?.subscription !== undefined)
|
|
177
|
+
rejected = [fieldErrors.subscription].flat().join(", ");
|
|
178
|
+
if (!fieldErrors || !(names.length || rejected !== undefined) || !names.every((n) => optional.includes(n)))
|
|
170
179
|
throw err;
|
|
171
|
-
const retry = { ...first.body
|
|
180
|
+
const retry = { ...first.body };
|
|
181
|
+
if (rejected !== undefined)
|
|
182
|
+
delete retry.subscription;
|
|
172
183
|
for (const field of names) {
|
|
173
184
|
delete retry[field];
|
|
174
185
|
dropped.push({ field, reason: field === "prs" ? `all PRs dropped: ${[fieldErrors[field]].flat().join(", ")}` : [fieldErrors[field]].flat().join(", ") });
|
package/dist/commands/hook.js
CHANGED
|
@@ -240,28 +240,32 @@ async function send(tool, file, env) {
|
|
|
240
240
|
saved = await request(cfg, "PATCH", path, body);
|
|
241
241
|
}
|
|
242
242
|
catch (err) {
|
|
243
|
+
if (isRefusal(err))
|
|
244
|
+
noteRefusal(env, repository, err.message);
|
|
243
245
|
if (!(err instanceof CliError && err.status === 404))
|
|
244
246
|
throw err;
|
|
245
|
-
// The plan is
|
|
246
|
-
// session needs a cost: 0 until prices load. Create-only: when an agent report created the
|
|
247
|
-
// only the machine's fields are sent. A server that ignores the header overwrites the
|
|
248
|
-
|
|
247
|
+
// The plan is that of BERLINER_VENDOR_SUBSCRIPTION_SLUG from the tool's environment, else the server's pick from the
|
|
248
|
+
// user's subscriptions. A new session needs a cost: 0 until prices load. Create-only: when an agent report created the
|
|
249
|
+
// session since the 404 (412), only the machine's fields are sent. A server that ignores the header overwrites the
|
|
250
|
+
// title, as before.
|
|
251
|
+
const create = (subscription) => request(cfg, "PUT", path, { ...body, cost_usd: body.cost_usd ?? 0, title: derivedTitle(facts.firstPrompt, git), subscription, team }, { headers: { "If-None-Match": "*" } }).catch((err) => {
|
|
249
252
|
if (err instanceof CliError && err.status === 412)
|
|
250
253
|
return request(cfg, "PATCH", path, body);
|
|
251
254
|
// Never a personal session instead: the next foreground command in this repository prints the refusal.
|
|
252
|
-
if (err
|
|
255
|
+
if (isRefusal(err))
|
|
253
256
|
noteRefusal(env, repository, err.message);
|
|
254
257
|
throw err;
|
|
255
258
|
});
|
|
256
259
|
try {
|
|
257
|
-
saved = await create(cfg.
|
|
260
|
+
saved = await create(cfg.subscription ?? undefined);
|
|
258
261
|
}
|
|
259
262
|
catch (err) {
|
|
260
|
-
// A
|
|
261
|
-
|
|
263
|
+
// A slug the server rejects must not lose the session: the server picks the plan instead.
|
|
264
|
+
const rejected = cfg.subscription && err instanceof CliError && err.status === 422 ? err.details?.fieldErrors?.subscription : undefined;
|
|
265
|
+
if (!rejected)
|
|
262
266
|
throw err;
|
|
263
267
|
saved = await create(undefined);
|
|
264
|
-
log(env, `${tool} ${sessionId}:
|
|
268
|
+
log(env, `${tool} ${sessionId}: BERLINER_VENDOR_SUBSCRIPTION_SLUG "${cfg.subscription}" isn't used: ${[rejected].flat().join(", ")}; the server picked the plan`);
|
|
265
269
|
}
|
|
266
270
|
}
|
|
267
271
|
const mismatch = teamWarning(saved.json.session.team, team ?? null);
|
|
@@ -269,6 +273,8 @@ async function send(tool, file, env) {
|
|
|
269
273
|
log(env, `${tool} ${sessionId}: ${mismatch}`);
|
|
270
274
|
await refreshPrs(cfg, path, saved.json.session.prs, env, `${tool} ${sessionId}`);
|
|
271
275
|
}
|
|
276
|
+
// A 422 the user must hear about: no plan, or a team the server refuses.
|
|
277
|
+
const isRefusal = (err) => err instanceof CliError && err.status === 422 && (err.code === "plan_required" || !!err.code?.startsWith("team_"));
|
|
272
278
|
// Untracked PRs keep their last reported state, so the machine's gh refreshes the open ones; the poller owns tracked PRs.
|
|
273
279
|
// In parallel, 5 seconds each, inside the child's deadline. Only changes are sent.
|
|
274
280
|
async function refreshPrs(cfg, path, prs, env, where) {
|
package/dist/commands/report.js
CHANGED
|
@@ -14,9 +14,9 @@ export async function report(cfg, values, positionals, env) {
|
|
|
14
14
|
const titled = values.title !== undefined;
|
|
15
15
|
if (tool)
|
|
16
16
|
values = await fromTool(tool, values, env, "report");
|
|
17
|
-
//
|
|
18
|
-
// cost has no prices to estimate it.
|
|
19
|
-
const missing = required.filter((flag) => values[flag] === undefined && !(flag === "plan" && cfg.
|
|
17
|
+
// --subscription or BERLINER_VENDOR_SUBSCRIPTION_SLUG stands in for --plan. Inside a tool, a report without a plan leaves it to the server,
|
|
18
|
+
// and one without a cost has no prices to estimate it.
|
|
19
|
+
const missing = required.filter((flag) => values[flag] === undefined && !(flag === "plan" && (values.subscription || cfg.subscription)) && !(tool && (flag === "plan" || flag === "cost")));
|
|
20
20
|
if (missing.length)
|
|
21
21
|
throw new CliError(`missing required flag(s): ${missing.map((f) => `--${f}`).join(", ")}`, "usage");
|
|
22
22
|
if (values.public && !values.evidence?.length)
|
|
@@ -24,14 +24,14 @@ export async function report(cfg, values, positionals, env) {
|
|
|
24
24
|
// Parsed before anything is saved, so a bad label saves nothing.
|
|
25
25
|
const evidence = (values.evidence ?? []).map(parseEvidence);
|
|
26
26
|
const team = await resolveTeam(values, process.cwd(), env);
|
|
27
|
-
const body =
|
|
27
|
+
const body = sessionBody(values);
|
|
28
28
|
const id = values["session-id"];
|
|
29
29
|
const path = `/v1/sessions/${encodeURIComponent(id)}`;
|
|
30
30
|
// Inside a tool a report without --title is usually a follow-up: the derived title is only for a new session. A report
|
|
31
31
|
// without a cost must not replace the saved one either.
|
|
32
32
|
const { json: saved, dropped } = await (tool && (!titled || body.cost_usd === undefined)
|
|
33
|
-
? patchFirst(cfg, path, body, titled,
|
|
34
|
-
: saveSession(cfg, "PUT", path, { ...body, team: team ?? undefined }
|
|
33
|
+
? patchFirst(cfg, path, body, titled, team)
|
|
34
|
+
: saveSession(cfg, "PUT", path, { ...body, team: team ?? undefined }));
|
|
35
35
|
const json = dropped.length ? { ...saved, dropped } : saved;
|
|
36
36
|
const { created, session } = json;
|
|
37
37
|
warnTeam(session, team);
|
package/dist/commands/summary.js
CHANGED
|
@@ -19,7 +19,7 @@ export async function summary(cfg, values) {
|
|
|
19
19
|
function subscriptionLines(c) {
|
|
20
20
|
if (!c)
|
|
21
21
|
return [];
|
|
22
|
-
const lines = c.rows.map((r) => `${subscriptionLabel(r)}: ${totalLabel(r.spend_usd, r.unpriced_sessions ?? 0)} used of $${r.prorated_usd.toFixed(2)} paid (${r.multiple.toFixed(1)}×), ${r.sessions} session(s)`);
|
|
22
|
+
const lines = c.rows.map((r) => `${subscriptionLabel(r)} (${r.slug}): ${totalLabel(r.spend_usd, r.unpriced_sessions ?? 0)} used of $${r.prorated_usd.toFixed(2)} paid (${r.multiple.toFixed(1)}×), ${r.sessions} session(s)`);
|
|
23
23
|
if (c.unmatched.sessions > 0)
|
|
24
24
|
lines.push(`On no listed plan: ${totalLabel(c.unmatched.spend_usd, c.unmatched.unpriced_sessions ?? 0)}, ${c.unmatched.sessions} session(s)`);
|
|
25
25
|
return lines;
|
package/dist/commands/update.js
CHANGED
|
@@ -18,11 +18,11 @@ export async function update(cfg, values, positionals, env) {
|
|
|
18
18
|
if (tool)
|
|
19
19
|
values = await fromTool(tool, { ...values, "session-id": positionals[0] ?? values["session-id"] }, env, "update");
|
|
20
20
|
const id = tool ? tool.sessionId : positionals[0];
|
|
21
|
-
// A plan is only sent when --plan is passed, so an update never changes it unasked
|
|
22
|
-
const body =
|
|
21
|
+
// A plan is only sent when --plan or --subscription is passed, so an update never changes it unasked.
|
|
22
|
+
const body = sessionBody(values);
|
|
23
23
|
const team = await resolveTeam(values, process.cwd(), env);
|
|
24
24
|
const path = `/v1/sessions/${encodeURIComponent(id)}`;
|
|
25
|
-
const { json: saved, dropped } = await (tool ? patchFirst(cfg, path, body, titled,
|
|
25
|
+
const { json: saved, dropped } = await (tool ? patchFirst(cfg, path, body, titled, team) : saveSession(cfg, "PATCH", path, body));
|
|
26
26
|
const json = dropped.length ? { ...saved, dropped } : saved;
|
|
27
27
|
warnTeam(json.session, team);
|
|
28
28
|
print({ data: json, text: `Updated session ${json.session.id}. ${json.session.pr_count} PR(s) linked.${projectHint(json.session)}` }, values.json === true);
|
package/dist/config.js
CHANGED
|
@@ -44,5 +44,9 @@ export function saveConfig(env, value) {
|
|
|
44
44
|
}
|
|
45
45
|
export function readConfig(env) {
|
|
46
46
|
const file = readConfigFile(env);
|
|
47
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
url: env.BERLINER_URL || file?.url || DEFAULT_URL,
|
|
49
|
+
token: env.BERLINER_TOKEN || file?.token || null,
|
|
50
|
+
subscription: env.BERLINER_VENDOR_SUBSCRIPTION_SLUG || null,
|
|
51
|
+
};
|
|
48
52
|
}
|
package/dist/index.js
CHANGED
|
@@ -24,11 +24,12 @@ const usage = `Usage: berliner <command> [flags]
|
|
|
24
24
|
|
|
25
25
|
Commands:
|
|
26
26
|
setup connect this terminal, install the CLI and skill [--yes --no-install --no-browser]
|
|
27
|
-
report --session-id <id> --title <t> --repo <owner/name> --cost <usd> --model <m> --plan <p>
|
|
27
|
+
report --session-id <id> --title <t> --repo <owner/name> --cost <usd> --model <m> --plan <p> | --subscription <slug>
|
|
28
28
|
[--task --task-url --project --started-at --ended-at --commits --coverage]
|
|
29
29
|
[--pr <url>[,merge=<state>][,ci=<state>]]... [--evidence <path>[::caption]]... [--public] [--json] [--strict]
|
|
30
30
|
inside Claude Code, Codex or OpenCode the session id, cost, model and repo are filled in,
|
|
31
|
-
and the server picks the plan (
|
|
31
|
+
and the server picks the plan (BERLINER_VENDOR_SUBSCRIPTION_SLUG overrides it for a new session)
|
|
32
|
+
--subscription puts the session on that subscription and its plan, over the variable
|
|
32
33
|
update <session-id> same flags as report, all optional (no session id inside those tools)
|
|
33
34
|
sessions [--repo --model --project --ci --merge --since <N>d | --from --to] [--limit <n>] [--json]
|
|
34
35
|
session <session-id> [--json]
|
|
@@ -48,7 +49,8 @@ Commands:
|
|
|
48
49
|
skill print the bundled SKILL.md
|
|
49
50
|
hook <tool> run by the hooks of claude-code, codex or opencode; reads the payload on stdin
|
|
50
51
|
|
|
51
|
-
Environment: BERLINER_TOKEN, BERLINER_URL,
|
|
52
|
+
Environment: BERLINER_TOKEN, BERLINER_URL, BERLINER_VENDOR_SUBSCRIPTION_SLUG (the subscription of new sessions),
|
|
53
|
+
BERLINER_TEAM`;
|
|
52
54
|
const args = parse(process.argv.slice(2));
|
|
53
55
|
// Reporting must never fail the agent's own pipeline unless it asks to (anti-goal 1).
|
|
54
56
|
const softFail = (args.command === "report" || args.command === "update") && !args.values.strict;
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -30,7 +30,7 @@ Hooks installed by `berliner setup` already report the session's cost, model, re
|
|
|
30
30
|
| `--evidence` | Each screenshot or video. |
|
|
31
31
|
| `--team`, `--no-team` | Only when the user asks for a one-off. See [Teams](#teams). |
|
|
32
32
|
|
|
33
|
-
Do not pass `--session-id`, `--cost` or `--model`: the CLI fills them in from the tool's own records, and an explicit value is ignored with a warning. `--repo` and `--plan` are not needed either: the plan comes from the user's `
|
|
33
|
+
Do not pass `--session-id`, `--cost` or `--model`: the CLI fills them in from the tool's own records, and an explicit value is ignored with a warning. `--repo` and `--plan` are not needed either: the plan comes from the user's `BERLINER_VENDOR_SUBSCRIPTION_SLUG`, or from their subscriptions in Settings. A report without `--title` keeps the session's existing title. `berliner update` ignores a session ID here.
|
|
34
34
|
|
|
35
35
|
## Name the session
|
|
36
36
|
|
|
@@ -137,7 +137,7 @@ Without a hook, `report` needs these flags:
|
|
|
137
137
|
| `--repo` | `owner/name`, from `git remote get-url origin`. |
|
|
138
138
|
| `--cost` | Estimated USD. Your harness's reported session cost if it shows one, otherwise estimate from token usage at the model's API prices. |
|
|
139
139
|
| `--model` | The primary model ID you ran on. |
|
|
140
|
-
| `--plan` | Your best guess at the plan you run on. Claude: `free`, `pro`, `max_x5`, `max_x20`, `team`, `enterprise`. OpenAI (Codex): `free`, `go`, `plus`, `pro`, `pro_more`, `pro_max`, `business`, `business_premium`. Billed per token: `api`. Always pass it: when the user has set `
|
|
140
|
+
| `--plan` | Your best guess at the plan you run on. Claude: `free`, `pro`, `max_x5`, `max_x20`, `team`, `enterprise`. OpenAI (Codex): `free`, `go`, `plus`, `pro`, `pro_more`, `pro_max`, `business`, `business_premium`. Billed per token: `api`. Always pass it: when the user has set `BERLINER_VENDOR_SUBSCRIPTION_SLUG`, that subscription's plan replaces yours. |
|
|
141
141
|
|
|
142
142
|
Report again with the same `--session-id` whenever something changes.
|
|
143
143
|
|
|
@@ -167,7 +167,7 @@ After the evidence line, it prints one line per file: `![Login page after the fi
|
|
|
167
167
|
|
|
168
168
|
## Environment
|
|
169
169
|
|
|
170
|
-
`berliner setup` stores the API token in a config file, so `BERLINER_TOKEN` is only needed to override it (a token from the Settings page at https://berliner.app). `BERLINER_URL` overrides the server address. `
|
|
170
|
+
`berliner setup` stores the API token in a config file, so `BERLINER_TOKEN` is only needed to override it (a token from the Settings page at https://berliner.app). `BERLINER_URL` overrides the server address. `BERLINER_VENDOR_SUBSCRIPTION_SLUG`, when the user sets it, names their subscription, and its plan replaces any `--plan`.
|
|
171
171
|
|
|
172
172
|
If the `berliner` command is not found, carry on with your work and say in your final message that `npx @paxnax/berliner@latest setup` needs to run. Do not try to install it yourself.
|
|
173
173
|
|