balladeer 1.0.13 → 1.0.15
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/checkout-form.d.ts +45 -0
- package/dist/checkout-form.js +223 -0
- package/dist/cli.js +52 -4
- package/dist/commands/guidance.d.ts +2 -10
- package/dist/commands/guidance.js +11 -13
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +53 -0
- package/dist/guidance-hook.mjs +162 -80
- package/dist/guidance.d.ts +2 -0
- package/dist/guidance.js +7 -1
- package/dist/hook-trust.d.ts +31 -0
- package/dist/hook-trust.js +63 -0
- package/dist/self-update.d.ts +29 -2
- package/dist/self-update.js +96 -11
- package/dist/user-scope.d.ts +2 -0
- package/dist/user-scope.js +26 -2
- package/dist/wire.d.ts +6 -2
- package/dist/wire.js +5 -1
- package/package.json +1 -1
package/dist/self-update.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { delimiter, dirname, join } from "node:path";
|
|
4
4
|
import { HOOK_SPECIFIER } from "./release.js";
|
|
5
5
|
import { configHome } from "./store.js";
|
|
6
6
|
/**
|
|
@@ -10,15 +10,33 @@ import { configHome } from "./store.js";
|
|
|
10
10
|
* update must never hold the agent. So the hook, having served this session
|
|
11
11
|
* from the copy it has, only starts the install: a detached child on the
|
|
12
12
|
* current major, output discarded, at most once a day per laptop whatever it
|
|
13
|
-
* reports. The next session runs whatever that child installed.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* reports. The next session runs whatever that child installed. `balladeer
|
|
14
|
+
* update` is the manual path. Nothing here reads the session or writes to it.
|
|
15
|
+
*
|
|
16
|
+
* What changed on 29 September 2026, from what Didero's laptops showed: four
|
|
17
|
+
* of nine stayed on 1.0.12 through two releases while active every day. The
|
|
18
|
+
* hook reached for `npx` on whatever PATH its host gave it, wrote "tried
|
|
19
|
+
* today" before it knew whether anything had started, and swallowed the
|
|
20
|
+
* failure, so a laptop whose editor launches hooks without npm on PATH failed
|
|
21
|
+
* silently once a day, for ever. Now the installer is launched through the
|
|
22
|
+
* node binary this hook is already running under, with npm's own npx script
|
|
23
|
+
* beside it; the day's stamp is written only once a child actually exists;
|
|
24
|
+
* and what happened is kept in a small state file the next hook fire reports
|
|
25
|
+
* to the server, so a laptop that cannot update is visible on the scorecard
|
|
26
|
+
* instead of looking merely behind.
|
|
16
27
|
*/
|
|
17
28
|
const STAMP = "self-update.json";
|
|
29
|
+
const STATE = "self-update-state.json";
|
|
18
30
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
31
|
+
/** Set on the child so the install it runs can record how it ended. */
|
|
32
|
+
export const SELF_UPDATE_ENVIRONMENT = "BALLADEER_SELF_UPDATE";
|
|
33
|
+
const STATE_PATTERN = /^(?:started|unstartable|installed|failed):[A-Za-z0-9._-]{1,40}$/;
|
|
19
34
|
function stampPath(environment) {
|
|
20
35
|
return join(configHome(environment), STAMP);
|
|
21
36
|
}
|
|
37
|
+
function statePath(environment) {
|
|
38
|
+
return join(configHome(environment), STATE);
|
|
39
|
+
}
|
|
22
40
|
function lastStarted(environment) {
|
|
23
41
|
const path = stampPath(environment);
|
|
24
42
|
if (!existsSync(path))
|
|
@@ -31,15 +49,71 @@ function lastStarted(environment) {
|
|
|
31
49
|
return 0;
|
|
32
50
|
}
|
|
33
51
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
52
|
+
function writePrivate(path, value, environment) {
|
|
53
|
+
mkdirSync(configHome(environment), { recursive: true, mode: 0o700 });
|
|
54
|
+
writeFileSync(path, JSON.stringify(value) + "\n", { mode: 0o600 });
|
|
55
|
+
}
|
|
56
|
+
/** What the last attempt came to, for the next hook fire to report. Never throws. */
|
|
57
|
+
export function recordSelfUpdateState(environment, state, at) {
|
|
58
|
+
if (!STATE_PATTERN.test(state))
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
writePrivate(statePath(environment), { state, at }, environment);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* A state that cannot be written is only a state nobody hears about. */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** The recorded state, or nothing when none was written or it is not one of ours. */
|
|
68
|
+
export function selfUpdateState(environment) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(readFileSync(statePath(environment), "utf8"));
|
|
71
|
+
return typeof parsed.state === "string" && STATE_PATTERN.test(parsed.state)
|
|
72
|
+
? parsed.state
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* How to start the installer without trusting PATH. npm ships npx as a script
|
|
81
|
+
* beside the node it belongs to, so the node this hook runs under can run it
|
|
82
|
+
* directly; that node certainly exists, which is what makes the launch
|
|
83
|
+
* dependable. The shim beside node is second, and PATH is last, for the
|
|
84
|
+
* layouts that keep npm somewhere else.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveLaunch(specifier, environment, execPath = process.execPath, exists = existsSync) {
|
|
87
|
+
const bin = dirname(execPath);
|
|
88
|
+
const tail = ["-y", specifier, "install", "--json"];
|
|
89
|
+
// The child's own children (npm's scripts) need to find node too.
|
|
90
|
+
const child = {
|
|
91
|
+
...environment,
|
|
92
|
+
PATH: [bin, environment.PATH ?? ""].filter((part) => part.length > 0).join(delimiter),
|
|
93
|
+
[SELF_UPDATE_ENVIRONMENT]: "1",
|
|
94
|
+
};
|
|
95
|
+
const script = join(bin, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js");
|
|
96
|
+
if (exists(script))
|
|
97
|
+
return { command: execPath, args: [script, ...tail], environment: child };
|
|
98
|
+
const shim = join(bin, process.platform === "win32" ? "npx.cmd" : "npx");
|
|
99
|
+
if (exists(shim))
|
|
100
|
+
return { command: shim, args: tail, environment: child };
|
|
101
|
+
return { command: "npx", args: tail, environment: child };
|
|
102
|
+
}
|
|
103
|
+
function detached(command, args, environment) {
|
|
104
|
+
const child = spawn(command, [...args], { detached: true, stdio: "ignore", env: environment });
|
|
36
105
|
child.on("error", () => undefined);
|
|
37
106
|
child.unref();
|
|
107
|
+
// A command that could not be found has no process id, and that is known
|
|
108
|
+
// before this function returns.
|
|
109
|
+
return child.pid !== undefined;
|
|
38
110
|
}
|
|
39
111
|
/**
|
|
40
112
|
* Start the update when the server named a newer release and none was
|
|
41
113
|
* started today. Answers whether one was started, for the caller's own
|
|
42
|
-
* record; the session hears nothing either way.
|
|
114
|
+
* record; the session hears nothing either way. A launch that could not start
|
|
115
|
+
* leaves no stamp, so the next session start tries again rather than waiting
|
|
116
|
+
* a day for the same failure.
|
|
43
117
|
*/
|
|
44
118
|
export function startSelfUpdateIfDue(newer, deps) {
|
|
45
119
|
if (newer === undefined)
|
|
@@ -47,13 +121,24 @@ export function startSelfUpdateIfDue(newer, deps) {
|
|
|
47
121
|
const now = deps.now ?? Date.now;
|
|
48
122
|
if (now() - lastStarted(deps.environment) < DAY_MS)
|
|
49
123
|
return false;
|
|
124
|
+
const launch = resolveLaunch(HOOK_SPECIFIER, deps.environment, deps.execPath, deps.exists);
|
|
125
|
+
let started;
|
|
50
126
|
try {
|
|
51
|
-
|
|
52
|
-
writeFileSync(stampPath(deps.environment), JSON.stringify({ startedAt: now(), toward: newer }) + "\n", { mode: 0o600 });
|
|
53
|
-
(deps.run ?? detached)("npx", ["-y", HOOK_SPECIFIER, "install", "--json"]);
|
|
54
|
-
return true;
|
|
127
|
+
started = (deps.run ?? detached)(launch.command, launch.args, launch.environment) !== false;
|
|
55
128
|
}
|
|
56
129
|
catch {
|
|
130
|
+
started = false;
|
|
131
|
+
}
|
|
132
|
+
if (!started) {
|
|
133
|
+
recordSelfUpdateState(deps.environment, "unstartable:launcher", now());
|
|
57
134
|
return false;
|
|
58
135
|
}
|
|
136
|
+
try {
|
|
137
|
+
writePrivate(stampPath(deps.environment), { startedAt: now(), toward: newer }, deps.environment);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* The child is running; an unwritten stamp costs one more attempt, not a missed one. */
|
|
141
|
+
}
|
|
142
|
+
recordSelfUpdateState(deps.environment, `started:${newer}`, now());
|
|
143
|
+
return true;
|
|
59
144
|
}
|
package/dist/user-scope.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export declare function isOurUserEntry(entry: unknown): boolean;
|
|
|
53
53
|
export declare function isInstalledCommandPath(command: string): boolean;
|
|
54
54
|
/** `~/.claude/settings.json` carries user-scope hooks. */
|
|
55
55
|
export declare function mergeClaudeUserHooks(home: string, published: boolean, installed?: string): UserScopeWrite;
|
|
56
|
+
/** What a person has to do once in Codex, which no command can do for them. */
|
|
57
|
+
export declare const CODEX_HOOK_APPROVAL = "Codex: type /hooks in a Codex session and trust Balladeer's three hooks. Codex skips a hook until you have trusted it, so until then Codex has Balladeer's tools and none of its session-start guidance or updates.";
|
|
56
58
|
/** `~/.codex/config.toml` carries both the server and the hooks for Codex, in
|
|
57
59
|
* one fenced block this command owns end to end. */
|
|
58
60
|
export declare function mergeCodexUserConfig(home: string, published: boolean, installed?: string): UserScopeWrite;
|
package/dist/user-scope.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { noteHooksWritten } from "./hook-trust.js";
|
|
1
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -207,6 +208,21 @@ export function mergeClaudeUserHooks(home, published, installed) {
|
|
|
207
208
|
return { host, path, status: "refused", reason: "permissions.allow is not a list" };
|
|
208
209
|
const deny = Array.isArray(permissions.deny) ? permissions.deny : [];
|
|
209
210
|
const denied = deny.some((rule) => typeof rule === "string" && /^mcp__balladeer(__|$)/.test(rule));
|
|
211
|
+
// An event this release no longer hooks: its own entry there is deprecated
|
|
212
|
+
// and comes out. Robert, 29 September 2026: a hook that should no longer
|
|
213
|
+
// exist is removed, a new one is added, one that still belongs persists.
|
|
214
|
+
for (const [event, rows] of Object.entries(hooks)) {
|
|
215
|
+
if (CLAUDE_EVENTS.includes(event) || !Array.isArray(rows))
|
|
216
|
+
continue;
|
|
217
|
+
const kept = rows.filter((row) => !row?.hooks?.some((h) => h.statusMessage === USER_SCOPE_OWNER));
|
|
218
|
+
if (kept.length === rows.length)
|
|
219
|
+
continue;
|
|
220
|
+
if (kept.length === 0)
|
|
221
|
+
delete hooks[event];
|
|
222
|
+
else
|
|
223
|
+
hooks[event] = kept;
|
|
224
|
+
changed = true;
|
|
225
|
+
}
|
|
210
226
|
if (!denied && !allow.some((rule) => rule === USER_SCOPE_ALLOW_RULE)) {
|
|
211
227
|
permissions.allow = [...allow, USER_SCOPE_ALLOW_RULE];
|
|
212
228
|
root.permissions = permissions;
|
|
@@ -219,6 +235,8 @@ export function mergeClaudeUserHooks(home, published, installed) {
|
|
|
219
235
|
writeJsonAtomically(path, JSON.stringify(root, null, 2) + "\n");
|
|
220
236
|
return { host, path, status: "written" };
|
|
221
237
|
}
|
|
238
|
+
/** What a person has to do once in Codex, which no command can do for them. */
|
|
239
|
+
export const CODEX_HOOK_APPROVAL = "Codex: type /hooks in a Codex session and trust Balladeer's three hooks. Codex skips a hook until you have trusted it, so until then Codex has Balladeer's tools and none of its session-start guidance or updates.";
|
|
222
240
|
const CODEX_START = "# balladeer:user:start";
|
|
223
241
|
const CODEX_END = "# balladeer:user:end";
|
|
224
242
|
/** `~/.codex/config.toml` carries both the server and the hooks for Codex, in
|
|
@@ -302,7 +320,13 @@ export function installUserScope(options) {
|
|
|
302
320
|
mergeClaudeUserMcp(home, options.published, options.installed),
|
|
303
321
|
mergeClaudeUserHooks(home, options.published, options.installed),
|
|
304
322
|
];
|
|
305
|
-
if (existsSync(join(home, ".codex")))
|
|
306
|
-
|
|
323
|
+
if (existsSync(join(home, ".codex"))) {
|
|
324
|
+
const codex = mergeCodexUserConfig(home, options.published, options.installed);
|
|
325
|
+
writes.push(codex);
|
|
326
|
+
// Codex has to run these once before anybody assumes it does; the mark is
|
|
327
|
+
// what lets its own session say so after a silent background update.
|
|
328
|
+
if (codex.status !== "refused")
|
|
329
|
+
noteHooksWritten(options.environment, "codex", codex.status === "written");
|
|
330
|
+
}
|
|
307
331
|
return writes;
|
|
308
332
|
}
|
package/dist/wire.d.ts
CHANGED
|
@@ -5,10 +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.15";
|
|
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.15`, `installed:1.0.15`, `failed:exit-1`, `unstartable:launcher`. */
|
|
12
|
+
export declare const UPDATE_STATE_HEADER = "x-balladeer-update-state";
|
|
13
|
+
/** Which host's hook made this guidance fetch: `claude` or `codex`. */
|
|
14
|
+
export declare const HOOK_HOST_HEADER = "x-balladeer-hook";
|
|
15
|
+
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.15";
|
|
12
16
|
export declare const DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
13
17
|
export type DelegatedScope = "repository:enroll" | "agent:issue" | "ci:connect" | "workspace:invite" | "candidate:propose";
|
|
14
18
|
export declare const DELEGATED_SCOPES: readonly DelegatedScope[];
|
package/dist/wire.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
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.15";
|
|
9
9
|
export const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export const CLIENT_HEADER = "x-balladeer-client";
|
|
11
|
+
/** What the last background update came to: `started:1.0.15`, `installed:1.0.15`, `failed:exit-1`, `unstartable:launcher`. */
|
|
12
|
+
export const UPDATE_STATE_HEADER = "x-balladeer-update-state";
|
|
13
|
+
/** Which host's hook made this guidance fetch: `claude` or `codex`. */
|
|
14
|
+
export const HOOK_HOST_HEADER = "x-balladeer-hook";
|
|
11
15
|
export const CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
|
|
12
16
|
export const DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
13
17
|
export const DELEGATED_SCOPES = [
|