balladeer 1.0.20 → 1.0.21
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/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +24 -1
- package/dist/currency.js +9 -3
- package/dist/guidance-hook.mjs +13 -3
- package/dist/self-update.d.ts +9 -0
- package/dist/self-update.js +35 -1
- package/dist/wire.d.ts +2 -2
- package/dist/wire.js +1 -1
- package/package.json +1 -1
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SelfUpdateDeps } from "../self-update.js";
|
|
1
2
|
import { type StoredAgent } from "../store.js";
|
|
2
3
|
export { forwarderHeaders, selectAgent, type AgentSelection } from "../agent.js";
|
|
3
4
|
export type McpOptions = Readonly<{
|
|
@@ -15,6 +16,9 @@ export type McpOptions = Readonly<{
|
|
|
15
16
|
stdin: NodeJS.ReadableStream;
|
|
16
17
|
write: (text: string) => void;
|
|
17
18
|
error: (text: string) => void;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
/** Runs the background update; injected so tests never reach npm. */
|
|
21
|
+
startUpdate?: SelfUpdateDeps["run"];
|
|
18
22
|
}>;
|
|
19
23
|
export type ProbeResult = Readonly<{
|
|
20
24
|
kind: "answered";
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { callAgentTool, forwarderHeaders, noAgentCredentialSentence, safeJson, selectAgent, structuredString, updateLine, } from "../agent.js";
|
|
3
|
-
import { noteServerVersion, updateNotice } from "../currency.js";
|
|
3
|
+
import { newerVersionPublished, noteServerVersion, updateNotice } from "../currency.js";
|
|
4
4
|
import { repositoryHint, repositoryHints } from "../repository.js";
|
|
5
5
|
import { CLI_VERSION } from "../wire.js";
|
|
6
6
|
import { untrackedLine } from "./guidance.js";
|
|
7
7
|
import { hooksAwaitingTrust, hooksAwaitingTrustNotice, trustingHost } from "../hook-trust.js";
|
|
8
|
+
import { startSelfUpdateIfDue } from "../self-update.js";
|
|
8
9
|
import { StoreError, readCredentials } from "../store.js";
|
|
9
10
|
// The three commands that use an agent connection select and address it through
|
|
10
11
|
// one module. These are re-exported because this is where the forwarder's
|
|
@@ -12,6 +13,8 @@ import { StoreError, readCredentials } from "../store.js";
|
|
|
12
13
|
export { forwarderHeaders, selectAgent } from "../agent.js";
|
|
13
14
|
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
14
15
|
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
/** How often one long-running forwarder looks at whether an update is due. */
|
|
17
|
+
const UPDATE_CHECK_MS = 60 * 60 * 1000;
|
|
15
18
|
/**
|
|
16
19
|
* One real call over the connection just issued, before anything claims it
|
|
17
20
|
* works.
|
|
@@ -195,6 +198,8 @@ export async function runMcp(options) {
|
|
|
195
198
|
// user-scope hook had just switched off. It installs nothing now.
|
|
196
199
|
const { agent } = selection;
|
|
197
200
|
let saidUpdate = false;
|
|
201
|
+
let updateCheckedAt = Number.NEGATIVE_INFINITY;
|
|
202
|
+
const clock = options.now ?? Date.now;
|
|
198
203
|
// Which host this session belongs to is known from its hello; whether its
|
|
199
204
|
// hooks are waiting for the person's trust is asked at the first tool call,
|
|
200
205
|
// by which time a trusted session-start hook has certainly run.
|
|
@@ -230,6 +235,24 @@ export async function runMcp(options) {
|
|
|
230
235
|
saidUpdate = true;
|
|
231
236
|
options.error(`${notice}\n`);
|
|
232
237
|
}
|
|
238
|
+
// The guidance hook is not the only way in. Codex skips a hook nobody has
|
|
239
|
+
// trusted, and a Codex-only laptop (Luke's, at Didero) then never started
|
|
240
|
+
// an update at all, while this forwarder ran every session. So the
|
|
241
|
+
// forwarder starts the same background install, under the same stamp,
|
|
242
|
+
// looking at most once an hour however many frames pass.
|
|
243
|
+
if (newerVersionPublished() !== undefined && clock() - updateCheckedAt >= UPDATE_CHECK_MS) {
|
|
244
|
+
updateCheckedAt = clock();
|
|
245
|
+
try {
|
|
246
|
+
startSelfUpdateIfDue(newerVersionPublished(), {
|
|
247
|
+
environment: options.environment,
|
|
248
|
+
now: clock,
|
|
249
|
+
...(options.startUpdate ? { run: options.startUpdate } : {}),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* An update that cannot even be considered never costs the session its tools. */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
233
256
|
if (response.status === 401) {
|
|
234
257
|
options.write(`${revokedConnectionFrame(frameId(line), options.controlPlane)}\n`);
|
|
235
258
|
options.error("Balladeer refused this agent connection. Rotate it in Balladeer.\n");
|
package/dist/currency.js
CHANGED
|
@@ -60,10 +60,16 @@ export function noteServerVersion(headers) {
|
|
|
60
60
|
newestSeen = newest;
|
|
61
61
|
const advice = headers.get(CLI_ADVICE_HEADER);
|
|
62
62
|
const line = advice === null ? "" : bounded(advice);
|
|
63
|
+
// On this copy's own major the background update installs it (it lands
|
|
64
|
+
// from 1.0.21), so there is nothing to tell anyone to run. Another major
|
|
65
|
+
// is a choice a person makes.
|
|
66
|
+
const sameMajor = newest.split(".")[0] === CLI_VERSION.split(".")[0];
|
|
63
67
|
notice =
|
|
64
|
-
line
|
|
65
|
-
?
|
|
66
|
-
:
|
|
68
|
+
line !== ""
|
|
69
|
+
? line
|
|
70
|
+
: sameMajor
|
|
71
|
+
? `A newer Balladeer is available (${newest}; this is ${CLI_VERSION}). It installs in the background; nothing needs to be run.`
|
|
72
|
+
: `A newer Balladeer is available (${newest}; this is ${CLI_VERSION}). Run npx -y balladeer@latest setup --refresh to update the command and instructions, then restart this session.`;
|
|
67
73
|
}
|
|
68
74
|
/** The one line this run prints about itself, or nothing when it is current. */
|
|
69
75
|
export function updateNotice() {
|
package/dist/guidance-hook.mjs
CHANGED
|
@@ -2152,7 +2152,7 @@ function relayHook(input, directory) {
|
|
|
2152
2152
|
}
|
|
2153
2153
|
|
|
2154
2154
|
// src/wire.ts
|
|
2155
|
-
var CLI_VERSION = "1.0.
|
|
2155
|
+
var CLI_VERSION = "1.0.21";
|
|
2156
2156
|
var CLIENT_HEADER = "x-balladeer-client";
|
|
2157
2157
|
var UPDATE_STATE_HEADER = "x-balladeer-update-state";
|
|
2158
2158
|
var HOOK_HOST_HEADER = "x-balladeer-hook";
|
|
@@ -2289,7 +2289,8 @@ function noteServerVersion(headers) {
|
|
|
2289
2289
|
newestSeen = newest;
|
|
2290
2290
|
const advice = headers.get(CLI_ADVICE_HEADER);
|
|
2291
2291
|
const line = advice === null ? "" : bounded(advice);
|
|
2292
|
-
|
|
2292
|
+
const sameMajor = newest.split(".")[0] === CLI_VERSION.split(".")[0];
|
|
2293
|
+
notice = line !== "" ? line : sameMajor ? `A newer Balladeer is available (${newest}; this is ${CLI_VERSION}). It installs in the background; nothing needs to be run.` : `A newer Balladeer is available (${newest}; this is ${CLI_VERSION}). Run npx -y balladeer@latest setup --refresh to update the command and instructions, then restart this session.`;
|
|
2293
2294
|
}
|
|
2294
2295
|
function newerVersionPublished() {
|
|
2295
2296
|
return newestSeen;
|
|
@@ -2334,6 +2335,13 @@ function due(newer, last, now) {
|
|
|
2334
2335
|
const newerTarget = last.toward === void 0 || older(last.toward, newer);
|
|
2335
2336
|
return newerTarget && since >= HOUR_MS;
|
|
2336
2337
|
}
|
|
2338
|
+
function updateSpecifier(newer) {
|
|
2339
|
+
if (!VERSION2.test(newer)) return void 0;
|
|
2340
|
+
const [name, major] = HOOK_SPECIFIER.split("@");
|
|
2341
|
+
if (newer.split(".")[0] !== major) return void 0;
|
|
2342
|
+
if (!older(CLI_VERSION, newer)) return void 0;
|
|
2343
|
+
return `${name}@${newer}`;
|
|
2344
|
+
}
|
|
2337
2345
|
function writePrivate(path, value, environment) {
|
|
2338
2346
|
mkdirSync3(configHome(environment), { recursive: true, mode: 448 });
|
|
2339
2347
|
writeFileSync2(path, JSON.stringify(value) + "\n", { mode: 384 });
|
|
@@ -2375,9 +2383,11 @@ function detached(command, args, environment) {
|
|
|
2375
2383
|
}
|
|
2376
2384
|
function startSelfUpdateIfDue(newer, deps) {
|
|
2377
2385
|
if (newer === void 0) return false;
|
|
2386
|
+
const specifier = updateSpecifier(newer);
|
|
2387
|
+
if (specifier === void 0) return false;
|
|
2378
2388
|
const now = deps.now ?? Date.now;
|
|
2379
2389
|
if (!due(newer, lastAttempt(deps.environment), now())) return false;
|
|
2380
|
-
const launch = resolveLaunch(
|
|
2390
|
+
const launch = resolveLaunch(specifier, deps.environment, deps.execPath, deps.exists);
|
|
2381
2391
|
let started;
|
|
2382
2392
|
try {
|
|
2383
2393
|
started = (deps.run ?? detached)(launch.command, launch.args, launch.environment) !== false;
|
package/dist/self-update.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export type SelfUpdateDeps = Readonly<{
|
|
|
16
16
|
}>;
|
|
17
17
|
/** A short, closed vocabulary: it travels in a header and lands in a column. */
|
|
18
18
|
export type SelfUpdateState = `started:${string}` | `unstartable:${string}` | `installed:${string}` | `failed:${string}`;
|
|
19
|
+
/**
|
|
20
|
+
* The exact release to ask npx for, or nothing when there is none to ask for.
|
|
21
|
+
*
|
|
22
|
+
* Exact, because a range is answered from whatever copy is already on the
|
|
23
|
+
* laptop (see above). Only a release on this copy's own major, so a 2.x stays a
|
|
24
|
+
* choice a person makes, and only one newer than this copy, so a server that
|
|
25
|
+
* names an older number cannot walk a laptop backwards.
|
|
26
|
+
*/
|
|
27
|
+
export declare function updateSpecifier(newer: string): string | undefined;
|
|
19
28
|
/** What the last attempt came to, for the next hook fire to report. Never throws. */
|
|
20
29
|
export declare function recordSelfUpdateState(environment: NodeJS.ProcessEnv, state: SelfUpdateState, at: number): void;
|
|
21
30
|
/** The recorded state, or nothing when none was written or it is not one of ours. */
|
package/dist/self-update.js
CHANGED
|
@@ -4,6 +4,7 @@ import { delimiter, dirname, join } from "node:path";
|
|
|
4
4
|
import { older } from "./currency.js";
|
|
5
5
|
import { HOOK_SPECIFIER } from "./release.js";
|
|
6
6
|
import { configHome } from "./store.js";
|
|
7
|
+
import { CLI_VERSION } from "./wire.js";
|
|
7
8
|
/**
|
|
8
9
|
* Bringing the durable command current without the session ever waiting.
|
|
9
10
|
*
|
|
@@ -35,6 +36,18 @@ import { configHome } from "./store.js";
|
|
|
35
36
|
* 1.0.15. Now any guidance hook fire can begin the install, and a release
|
|
36
37
|
* newer than the one the last attempt aimed at may begin one an hour after
|
|
37
38
|
* it, rather than a day.
|
|
39
|
+
*
|
|
40
|
+
* What changed on 14 October 2026, from Didero's laptops and Robert's again:
|
|
41
|
+
* seven laptops sat on 1.0.12 to 1.0.14 for two weeks, and Robert's on 1.0.19
|
|
42
|
+
* with an attempt toward 1.0.20 "started" that morning. Every attempt had
|
|
43
|
+
* asked npx for `balladeer@1`, a range. npx runs any copy already on the
|
|
44
|
+
* laptop that satisfies a range without asking the registry: the durable
|
|
45
|
+
* command itself when it sits in npm's global folder, or whatever the first
|
|
46
|
+
* attempt left in npx's own cache. That copy's installer installs its own
|
|
47
|
+
* version and refuses to go down, so every attempt after the first changed
|
|
48
|
+
* nothing, silently, once a day. Now the attempt names the exact release the
|
|
49
|
+
* server announced, which npx cannot satisfy from an older copy, and the
|
|
50
|
+
* installer that runs is the one being installed.
|
|
38
51
|
*/
|
|
39
52
|
const STAMP = "self-update.json";
|
|
40
53
|
const STATE = "self-update-state.json";
|
|
@@ -79,6 +92,24 @@ function due(newer, last, now) {
|
|
|
79
92
|
const newerTarget = last.toward === undefined || older(last.toward, newer);
|
|
80
93
|
return newerTarget && since >= HOUR_MS;
|
|
81
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The exact release to ask npx for, or nothing when there is none to ask for.
|
|
97
|
+
*
|
|
98
|
+
* Exact, because a range is answered from whatever copy is already on the
|
|
99
|
+
* laptop (see above). Only a release on this copy's own major, so a 2.x stays a
|
|
100
|
+
* choice a person makes, and only one newer than this copy, so a server that
|
|
101
|
+
* names an older number cannot walk a laptop backwards.
|
|
102
|
+
*/
|
|
103
|
+
export function updateSpecifier(newer) {
|
|
104
|
+
if (!VERSION.test(newer))
|
|
105
|
+
return undefined;
|
|
106
|
+
const [name, major] = HOOK_SPECIFIER.split("@");
|
|
107
|
+
if (newer.split(".")[0] !== major)
|
|
108
|
+
return undefined;
|
|
109
|
+
if (!older(CLI_VERSION, newer))
|
|
110
|
+
return undefined;
|
|
111
|
+
return `${name}@${newer}`;
|
|
112
|
+
}
|
|
82
113
|
function writePrivate(path, value, environment) {
|
|
83
114
|
mkdirSync(configHome(environment), { recursive: true, mode: 0o700 });
|
|
84
115
|
writeFileSync(path, JSON.stringify(value) + "\n", { mode: 0o600 });
|
|
@@ -148,10 +179,13 @@ function detached(command, args, environment) {
|
|
|
148
179
|
export function startSelfUpdateIfDue(newer, deps) {
|
|
149
180
|
if (newer === undefined)
|
|
150
181
|
return false;
|
|
182
|
+
const specifier = updateSpecifier(newer);
|
|
183
|
+
if (specifier === undefined)
|
|
184
|
+
return false;
|
|
151
185
|
const now = deps.now ?? Date.now;
|
|
152
186
|
if (!due(newer, lastAttempt(deps.environment), now()))
|
|
153
187
|
return false;
|
|
154
|
-
const launch = resolveLaunch(
|
|
188
|
+
const launch = resolveLaunch(specifier, deps.environment, deps.execPath, deps.exists);
|
|
155
189
|
let started;
|
|
156
190
|
try {
|
|
157
191
|
started = (deps.run ?? detached)(launch.command, launch.args, launch.environment) !== false;
|
package/dist/wire.d.ts
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export declare const CLI_VERSION = "1.0.
|
|
8
|
+
export declare const CLI_VERSION = "1.0.21";
|
|
9
9
|
export declare const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export declare const CLIENT_HEADER = "x-balladeer-client";
|
|
11
11
|
/** What the last background update came to: `started:1.0.17`, `installed:1.0.17`, `failed:exit-1`, `unstartable:launcher`. */
|
|
12
12
|
export declare const UPDATE_STATE_HEADER = "x-balladeer-update-state";
|
|
13
13
|
/** Which host's hook made this guidance fetch: `claude` or `codex`. */
|
|
14
14
|
export declare const HOOK_HOST_HEADER = "x-balladeer-hook";
|
|
15
|
-
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.
|
|
15
|
+
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.21";
|
|
16
16
|
export declare const DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
17
17
|
export type DelegatedScope = "repository:enroll" | "agent:issue" | "ci:connect" | "workspace:invite" | "candidate:propose";
|
|
18
18
|
export declare const DELEGATED_SCOPES: readonly DelegatedScope[];
|
package/dist/wire.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export const CLI_VERSION = "1.0.
|
|
8
|
+
export const CLI_VERSION = "1.0.21";
|
|
9
9
|
export const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export const CLIENT_HEADER = "x-balladeer-client";
|
|
11
11
|
/** What the last background update came to: `started:1.0.17`, `installed:1.0.17`, `failed:exit-1`, `unstartable:launcher`. */
|