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/guidance-hook.mjs
CHANGED
|
@@ -2027,8 +2027,10 @@ var require_toml = __commonJS({
|
|
|
2027
2027
|
});
|
|
2028
2028
|
|
|
2029
2029
|
// src/wire.ts
|
|
2030
|
-
var CLI_VERSION = "1.0.
|
|
2030
|
+
var CLI_VERSION = "1.0.15";
|
|
2031
2031
|
var CLIENT_HEADER = "x-balladeer-client";
|
|
2032
|
+
var UPDATE_STATE_HEADER = "x-balladeer-update-state";
|
|
2033
|
+
var HOOK_HOST_HEADER = "x-balladeer-hook";
|
|
2032
2034
|
var CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
|
|
2033
2035
|
var DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
2034
2036
|
|
|
@@ -2150,6 +2152,102 @@ function newerVersionPublished() {
|
|
|
2150
2152
|
return newestSeen;
|
|
2151
2153
|
}
|
|
2152
2154
|
|
|
2155
|
+
// src/self-update.ts
|
|
2156
|
+
import { spawn } from "node:child_process";
|
|
2157
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
2158
|
+
import { delimiter, dirname as dirname2, join as join2 } from "node:path";
|
|
2159
|
+
|
|
2160
|
+
// src/release.ts
|
|
2161
|
+
var HOOK_SPECIFIER = "balladeer@1";
|
|
2162
|
+
|
|
2163
|
+
// src/self-update.ts
|
|
2164
|
+
var STAMP = "self-update.json";
|
|
2165
|
+
var STATE = "self-update-state.json";
|
|
2166
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2167
|
+
var SELF_UPDATE_ENVIRONMENT = "BALLADEER_SELF_UPDATE";
|
|
2168
|
+
var STATE_PATTERN = /^(?:started|unstartable|installed|failed):[A-Za-z0-9._-]{1,40}$/;
|
|
2169
|
+
function stampPath(environment) {
|
|
2170
|
+
return join2(configHome(environment), STAMP);
|
|
2171
|
+
}
|
|
2172
|
+
function statePath(environment) {
|
|
2173
|
+
return join2(configHome(environment), STATE);
|
|
2174
|
+
}
|
|
2175
|
+
function lastStarted(environment) {
|
|
2176
|
+
const path = stampPath(environment);
|
|
2177
|
+
if (!existsSync(path)) return 0;
|
|
2178
|
+
try {
|
|
2179
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
2180
|
+
return typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
|
|
2181
|
+
} catch {
|
|
2182
|
+
return 0;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function writePrivate(path, value, environment) {
|
|
2186
|
+
mkdirSync2(configHome(environment), { recursive: true, mode: 448 });
|
|
2187
|
+
writeFileSync(path, JSON.stringify(value) + "\n", { mode: 384 });
|
|
2188
|
+
}
|
|
2189
|
+
function recordSelfUpdateState(environment, state, at) {
|
|
2190
|
+
if (!STATE_PATTERN.test(state)) return;
|
|
2191
|
+
try {
|
|
2192
|
+
writePrivate(statePath(environment), { state, at }, environment);
|
|
2193
|
+
} catch {
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
function selfUpdateState(environment) {
|
|
2197
|
+
try {
|
|
2198
|
+
const parsed = JSON.parse(readFileSync2(statePath(environment), "utf8"));
|
|
2199
|
+
return typeof parsed.state === "string" && STATE_PATTERN.test(parsed.state) ? parsed.state : void 0;
|
|
2200
|
+
} catch {
|
|
2201
|
+
return void 0;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
function resolveLaunch(specifier, environment, execPath = process.execPath, exists = existsSync) {
|
|
2205
|
+
const bin = dirname2(execPath);
|
|
2206
|
+
const tail = ["-y", specifier, "install", "--json"];
|
|
2207
|
+
const child = {
|
|
2208
|
+
...environment,
|
|
2209
|
+
PATH: [bin, environment.PATH ?? ""].filter((part) => part.length > 0).join(delimiter),
|
|
2210
|
+
[SELF_UPDATE_ENVIRONMENT]: "1"
|
|
2211
|
+
};
|
|
2212
|
+
const script = join2(bin, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js");
|
|
2213
|
+
if (exists(script)) return { command: execPath, args: [script, ...tail], environment: child };
|
|
2214
|
+
const shim = join2(bin, process.platform === "win32" ? "npx.cmd" : "npx");
|
|
2215
|
+
if (exists(shim)) return { command: shim, args: tail, environment: child };
|
|
2216
|
+
return { command: "npx", args: tail, environment: child };
|
|
2217
|
+
}
|
|
2218
|
+
function detached(command, args, environment) {
|
|
2219
|
+
const child = spawn(command, [...args], { detached: true, stdio: "ignore", env: environment });
|
|
2220
|
+
child.on("error", () => void 0);
|
|
2221
|
+
child.unref();
|
|
2222
|
+
return child.pid !== void 0;
|
|
2223
|
+
}
|
|
2224
|
+
function startSelfUpdateIfDue(newer, deps) {
|
|
2225
|
+
if (newer === void 0) return false;
|
|
2226
|
+
const now = deps.now ?? Date.now;
|
|
2227
|
+
if (now() - lastStarted(deps.environment) < DAY_MS) return false;
|
|
2228
|
+
const launch = resolveLaunch(HOOK_SPECIFIER, deps.environment, deps.execPath, deps.exists);
|
|
2229
|
+
let started;
|
|
2230
|
+
try {
|
|
2231
|
+
started = (deps.run ?? detached)(launch.command, launch.args, launch.environment) !== false;
|
|
2232
|
+
} catch {
|
|
2233
|
+
started = false;
|
|
2234
|
+
}
|
|
2235
|
+
if (!started) {
|
|
2236
|
+
recordSelfUpdateState(deps.environment, "unstartable:launcher", now());
|
|
2237
|
+
return false;
|
|
2238
|
+
}
|
|
2239
|
+
try {
|
|
2240
|
+
writePrivate(
|
|
2241
|
+
stampPath(deps.environment),
|
|
2242
|
+
{ startedAt: now(), toward: newer },
|
|
2243
|
+
deps.environment
|
|
2244
|
+
);
|
|
2245
|
+
} catch {
|
|
2246
|
+
}
|
|
2247
|
+
recordSelfUpdateState(deps.environment, `started:${newer}`, now());
|
|
2248
|
+
return true;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2153
2251
|
// src/guidance.ts
|
|
2154
2252
|
import { createHash, randomBytes } from "node:crypto";
|
|
2155
2253
|
import {
|
|
@@ -2157,14 +2255,14 @@ import {
|
|
|
2157
2255
|
closeSync as closeSync2,
|
|
2158
2256
|
fstatSync,
|
|
2159
2257
|
lstatSync as lstatSync2,
|
|
2160
|
-
mkdirSync as
|
|
2258
|
+
mkdirSync as mkdirSync3,
|
|
2161
2259
|
openSync as openSync2,
|
|
2162
|
-
readFileSync as
|
|
2260
|
+
readFileSync as readFileSync3,
|
|
2163
2261
|
renameSync as renameSync2,
|
|
2164
2262
|
unlinkSync as unlinkSync2,
|
|
2165
|
-
writeFileSync
|
|
2263
|
+
writeFileSync as writeFileSync2
|
|
2166
2264
|
} from "node:fs";
|
|
2167
|
-
import { join as
|
|
2265
|
+
import { join as join3 } from "node:path";
|
|
2168
2266
|
var GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
|
|
2169
2267
|
var HEX = /^[a-f0-9]{64}$/;
|
|
2170
2268
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
@@ -2201,7 +2299,7 @@ function readJson(path) {
|
|
|
2201
2299
|
fd = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
2202
2300
|
const stat = fstatSync(fd);
|
|
2203
2301
|
if (!stat.isFile() || stat.size > MAX_BYTES || (stat.mode & 63) !== 0) return void 0;
|
|
2204
|
-
return JSON.parse(
|
|
2302
|
+
return JSON.parse(readFileSync3(fd, "utf8"));
|
|
2205
2303
|
} catch {
|
|
2206
2304
|
return void 0;
|
|
2207
2305
|
} finally {
|
|
@@ -2210,7 +2308,7 @@ function readJson(path) {
|
|
|
2210
2308
|
}
|
|
2211
2309
|
function privateDirectory(path) {
|
|
2212
2310
|
try {
|
|
2213
|
-
|
|
2311
|
+
mkdirSync3(path, { mode: 448 });
|
|
2214
2312
|
} catch {
|
|
2215
2313
|
}
|
|
2216
2314
|
const stat = lstatSync2(path);
|
|
@@ -2220,7 +2318,7 @@ function privateDirectory(path) {
|
|
|
2220
2318
|
function atomicJson(path, value) {
|
|
2221
2319
|
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
2222
2320
|
try {
|
|
2223
|
-
|
|
2321
|
+
writeFileSync2(temporary, `${JSON.stringify(value)}
|
|
2224
2322
|
`, { flag: "wx", mode: 384 });
|
|
2225
2323
|
renameSync2(temporary, path);
|
|
2226
2324
|
} finally {
|
|
@@ -2244,16 +2342,16 @@ function guidanceScopeKey(agent) {
|
|
|
2244
2342
|
function cacheDirectory(environment) {
|
|
2245
2343
|
const home = configHome(environment);
|
|
2246
2344
|
privateDirectory(home);
|
|
2247
|
-
const directory =
|
|
2345
|
+
const directory = join3(home, "guidance-v1");
|
|
2248
2346
|
privateDirectory(directory);
|
|
2249
2347
|
return directory;
|
|
2250
2348
|
}
|
|
2251
2349
|
function cached(directory, scopeKey, agent) {
|
|
2252
2350
|
try {
|
|
2253
|
-
const pointer = readJson(
|
|
2351
|
+
const pointer = readJson(join3(directory, `${scopeKey}.current.json`));
|
|
2254
2352
|
if (!object(pointer) || typeof pointer.key !== "string" || !HEX.test(pointer.key))
|
|
2255
2353
|
return void 0;
|
|
2256
|
-
const body = readJson(
|
|
2354
|
+
const body = readJson(join3(directory, `${pointer.key}.body.json`));
|
|
2257
2355
|
if (!object(body) || typeof body.etag !== "string" || !/^"[a-f0-9]{64}"$/.test(body.etag))
|
|
2258
2356
|
return void 0;
|
|
2259
2357
|
const document = parseGuidance(body.document, agent);
|
|
@@ -2291,6 +2389,7 @@ async function loadGuidance(input) {
|
|
|
2291
2389
|
} catch {
|
|
2292
2390
|
}
|
|
2293
2391
|
const previous = directory ? cached(directory, scopeKey, input.agent) : void 0;
|
|
2392
|
+
const updateState = selfUpdateState(input.environment);
|
|
2294
2393
|
const abort = new AbortController();
|
|
2295
2394
|
let timer;
|
|
2296
2395
|
try {
|
|
@@ -2302,6 +2401,10 @@ async function loadGuidance(input) {
|
|
|
2302
2401
|
headers: {
|
|
2303
2402
|
authorization: `Bearer ${input.agent.token}`,
|
|
2304
2403
|
[CLIENT_HEADER]: CLIENT_HEADER_VALUE,
|
|
2404
|
+
// How the last background update ended, so a laptop that cannot
|
|
2405
|
+
// update itself is seen on the scorecard rather than guessed at.
|
|
2406
|
+
...updateState === void 0 ? {} : { [UPDATE_STATE_HEADER]: updateState },
|
|
2407
|
+
...input.hook === void 0 ? {} : { [HOOK_HOST_HEADER]: input.hook },
|
|
2305
2408
|
accept: "application/json",
|
|
2306
2409
|
...previous ? { "if-none-match": previous.etag } : {}
|
|
2307
2410
|
}
|
|
@@ -2325,11 +2428,11 @@ async function loadGuidance(input) {
|
|
|
2325
2428
|
if (directory) {
|
|
2326
2429
|
try {
|
|
2327
2430
|
const key = sha(JSON.stringify([scopeKey, document.revision]));
|
|
2328
|
-
atomicJson(
|
|
2329
|
-
atomicJson(
|
|
2431
|
+
atomicJson(join3(directory, `${key}.body.json`), { document, etag });
|
|
2432
|
+
atomicJson(join3(directory, `${scopeKey}.current.json`), { key });
|
|
2330
2433
|
if (previous && previous.key !== key) {
|
|
2331
2434
|
try {
|
|
2332
|
-
unlinkSync2(
|
|
2435
|
+
unlinkSync2(join3(directory, `${previous.key}.body.json`));
|
|
2333
2436
|
} catch {
|
|
2334
2437
|
}
|
|
2335
2438
|
}
|
|
@@ -2357,7 +2460,7 @@ async function loadGuidance(input) {
|
|
|
2357
2460
|
if (timer) clearTimeout(timer);
|
|
2358
2461
|
}
|
|
2359
2462
|
}
|
|
2360
|
-
function recordGuidanceContext(input,
|
|
2463
|
+
function recordGuidanceContext(input, write2) {
|
|
2361
2464
|
const always = input.event !== "UserPromptSubmit" || input.load.status === "unavailable" || !input.sessionId;
|
|
2362
2465
|
const contextKey = sha(
|
|
2363
2466
|
JSON.stringify([
|
|
@@ -2371,13 +2474,13 @@ function recordGuidanceContext(input, write) {
|
|
|
2371
2474
|
let path;
|
|
2372
2475
|
let emitted = true;
|
|
2373
2476
|
try {
|
|
2374
|
-
path =
|
|
2477
|
+
path = join3(cacheDirectory(input.environment), `${contextKey}.context.json`);
|
|
2375
2478
|
const previous = readJson(path);
|
|
2376
2479
|
emitted = always || !object(previous) || previous.status === "unavailable" || previous.digest !== document?.digest || previous.revision !== document?.revision || (previous.runtimeGeneration ?? null) !== (input.runtimeGeneration ?? null);
|
|
2377
2480
|
} catch {
|
|
2378
2481
|
}
|
|
2379
2482
|
if (!emitted) return false;
|
|
2380
|
-
|
|
2483
|
+
write2();
|
|
2381
2484
|
if (path) {
|
|
2382
2485
|
try {
|
|
2383
2486
|
atomicJson(path, {
|
|
@@ -2404,50 +2507,45 @@ import {
|
|
|
2404
2507
|
closeSync as closeSync5,
|
|
2405
2508
|
fstatSync as fstatSync2,
|
|
2406
2509
|
openSync as openSync5,
|
|
2407
|
-
existsSync as
|
|
2510
|
+
existsSync as existsSync3,
|
|
2408
2511
|
lstatSync as lstatSync4,
|
|
2409
2512
|
linkSync,
|
|
2410
|
-
mkdirSync as
|
|
2411
|
-
readFileSync as
|
|
2513
|
+
mkdirSync as mkdirSync5,
|
|
2514
|
+
readFileSync as readFileSync6,
|
|
2412
2515
|
renameSync as renameSync5,
|
|
2413
2516
|
unlinkSync as unlinkSync5,
|
|
2414
|
-
writeFileSync as
|
|
2517
|
+
writeFileSync as writeFileSync3
|
|
2415
2518
|
} from "node:fs";
|
|
2416
2519
|
var import_toml2 = __toESM(require_toml(), 1);
|
|
2417
|
-
import { dirname as
|
|
2520
|
+
import { dirname as dirname4, join as join6, relative } from "node:path";
|
|
2418
2521
|
|
|
2419
2522
|
// src/codex-config.ts
|
|
2420
2523
|
var import_toml = __toESM(require_toml(), 1);
|
|
2421
2524
|
import {
|
|
2422
2525
|
closeSync as closeSync4,
|
|
2423
|
-
existsSync,
|
|
2526
|
+
existsSync as existsSync2,
|
|
2424
2527
|
fsyncSync as fsyncSync3,
|
|
2425
2528
|
lstatSync as lstatSync3,
|
|
2426
|
-
mkdirSync as
|
|
2529
|
+
mkdirSync as mkdirSync4,
|
|
2427
2530
|
openSync as openSync4,
|
|
2428
|
-
readFileSync as
|
|
2531
|
+
readFileSync as readFileSync5,
|
|
2429
2532
|
renameSync as renameSync4,
|
|
2430
2533
|
unlinkSync as unlinkSync4,
|
|
2431
2534
|
writeSync as writeSync3
|
|
2432
2535
|
} from "node:fs";
|
|
2433
|
-
import { join as
|
|
2536
|
+
import { join as join5 } from "node:path";
|
|
2434
2537
|
|
|
2435
2538
|
// src/mcp-config.ts
|
|
2436
2539
|
import {
|
|
2437
2540
|
closeSync as closeSync3,
|
|
2438
2541
|
fsyncSync as fsyncSync2,
|
|
2439
2542
|
openSync as openSync3,
|
|
2440
|
-
readFileSync as
|
|
2543
|
+
readFileSync as readFileSync4,
|
|
2441
2544
|
renameSync as renameSync3,
|
|
2442
2545
|
unlinkSync as unlinkSync3,
|
|
2443
2546
|
writeSync as writeSync2
|
|
2444
2547
|
} from "node:fs";
|
|
2445
|
-
import { basename, dirname as
|
|
2446
|
-
|
|
2447
|
-
// src/release.ts
|
|
2448
|
-
var HOOK_SPECIFIER = "balladeer@1";
|
|
2449
|
-
|
|
2450
|
-
// src/mcp-config.ts
|
|
2548
|
+
import { basename, dirname as dirname3, isAbsolute, join as join4 } from "node:path";
|
|
2451
2549
|
var MCP_CONFIG_FILE = ".mcp.json";
|
|
2452
2550
|
function sameOrigin(left, right) {
|
|
2453
2551
|
try {
|
|
@@ -2490,7 +2588,7 @@ function currentEntry(parsed) {
|
|
|
2490
2588
|
}
|
|
2491
2589
|
function readMcpConfig(repositoryRoot) {
|
|
2492
2590
|
try {
|
|
2493
|
-
return JSON.parse(
|
|
2591
|
+
return JSON.parse(readFileSync4(join4(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
|
|
2494
2592
|
} catch {
|
|
2495
2593
|
return void 0;
|
|
2496
2594
|
}
|
|
@@ -2500,7 +2598,7 @@ function readMcpConfig(repositoryRoot) {
|
|
|
2500
2598
|
var CODEX_CONFIG_FILE = ".codex/config.toml";
|
|
2501
2599
|
function readCodexEntry(root) {
|
|
2502
2600
|
try {
|
|
2503
|
-
const config = (0, import_toml.parse)(
|
|
2601
|
+
const config = (0, import_toml.parse)(readFileSync5(join5(root, CODEX_CONFIG_FILE), "utf8"));
|
|
2504
2602
|
const servers = config.mcp_servers;
|
|
2505
2603
|
return servers && typeof servers === "object" && !Array.isArray(servers) && !(servers instanceof Date) ? servers.balladeer : void 0;
|
|
2506
2604
|
} catch {
|
|
@@ -2509,7 +2607,7 @@ function readCodexEntry(root) {
|
|
|
2509
2607
|
}
|
|
2510
2608
|
function hasManagedCodexEntry(root, controlPlane) {
|
|
2511
2609
|
try {
|
|
2512
|
-
const text =
|
|
2610
|
+
const text = readFileSync5(join5(root, CODEX_CONFIG_FILE), "utf8");
|
|
2513
2611
|
const starts = [...text.matchAll(/^# balladeer:mcp:start\r?$/gm)];
|
|
2514
2612
|
const ends = [...text.matchAll(/^# balladeer:mcp:end\r?$/gm)];
|
|
2515
2613
|
if (starts.length !== 1 || ends.length !== 1 || !starts[0] || !ends[0] || starts[0].index >= ends[0].index)
|
|
@@ -2853,7 +2951,7 @@ in for them to read. The tool sends nothing itself: an administrator presses Sen
|
|
|
2853
2951
|
// src/guidance-install.ts
|
|
2854
2952
|
var MAX_FILE = 262144;
|
|
2855
2953
|
function readRegular(path, allowMissing = false, writable = true) {
|
|
2856
|
-
if (!
|
|
2954
|
+
if (!existsSync3(path)) {
|
|
2857
2955
|
try {
|
|
2858
2956
|
lstatSync4(path);
|
|
2859
2957
|
} catch (e) {
|
|
@@ -2864,10 +2962,10 @@ function readRegular(path, allowMissing = false, writable = true) {
|
|
|
2864
2962
|
const stat = lstatSync4(path);
|
|
2865
2963
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE || writable && (stat.mode & 146) === 0)
|
|
2866
2964
|
throw new Error("file_not_safely_writable");
|
|
2867
|
-
return
|
|
2965
|
+
return readFileSync6(path, "utf8");
|
|
2868
2966
|
}
|
|
2869
2967
|
function safeParent(root, path) {
|
|
2870
|
-
let at =
|
|
2968
|
+
let at = dirname4(path);
|
|
2871
2969
|
while (at !== root) {
|
|
2872
2970
|
if (relative(root, at).startsWith("..")) throw new Error("outside_repository");
|
|
2873
2971
|
try {
|
|
@@ -2876,7 +2974,7 @@ function safeParent(root, path) {
|
|
|
2876
2974
|
} catch (e) {
|
|
2877
2975
|
if (e.code !== "ENOENT") throw e;
|
|
2878
2976
|
}
|
|
2879
|
-
at =
|
|
2977
|
+
at = dirname4(at);
|
|
2880
2978
|
}
|
|
2881
2979
|
}
|
|
2882
2980
|
function validateProjectGuidanceScope(options) {
|
|
@@ -2886,7 +2984,7 @@ function validateProjectGuidanceScope(options) {
|
|
|
2886
2984
|
stdio: ["ignore", "pipe", "ignore"],
|
|
2887
2985
|
timeout: 1e3
|
|
2888
2986
|
}).trim();
|
|
2889
|
-
const path =
|
|
2987
|
+
const path = join6(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
|
|
2890
2988
|
safeParent(root, path);
|
|
2891
2989
|
readRegular(path, false, false);
|
|
2892
2990
|
const entry = options.hook === "codex" ? readCodexEntry(root) : currentEntry(readMcpConfig(root));
|
|
@@ -2999,47 +3097,33 @@ function repositoryHints(cwd = process.cwd()) {
|
|
|
2999
3097
|
return hints;
|
|
3000
3098
|
}
|
|
3001
3099
|
|
|
3002
|
-
// src/
|
|
3003
|
-
import {
|
|
3004
|
-
import {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3008
|
-
function stampPath(environment) {
|
|
3009
|
-
return join6(configHome(environment), STAMP);
|
|
3010
|
-
}
|
|
3011
|
-
function lastStarted(environment) {
|
|
3012
|
-
const path = stampPath(environment);
|
|
3013
|
-
if (!existsSync3(path)) return 0;
|
|
3100
|
+
// src/hook-trust.ts
|
|
3101
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3102
|
+
import { join as join7 } from "node:path";
|
|
3103
|
+
var FILE = "hook-trust.json";
|
|
3104
|
+
function read(environment) {
|
|
3014
3105
|
try {
|
|
3015
|
-
const parsed = JSON.parse(
|
|
3016
|
-
return typeof parsed
|
|
3106
|
+
const parsed = JSON.parse(readFileSync7(join7(configHome(environment), FILE), "utf8"));
|
|
3107
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3017
3108
|
} catch {
|
|
3018
|
-
return
|
|
3109
|
+
return {};
|
|
3019
3110
|
}
|
|
3020
3111
|
}
|
|
3021
|
-
function
|
|
3022
|
-
const child = spawn(command, [...args], { detached: true, stdio: "ignore" });
|
|
3023
|
-
child.on("error", () => void 0);
|
|
3024
|
-
child.unref();
|
|
3025
|
-
}
|
|
3026
|
-
function startSelfUpdateIfDue(newer, deps) {
|
|
3027
|
-
if (newer === void 0) return false;
|
|
3028
|
-
const now = deps.now ?? Date.now;
|
|
3029
|
-
if (now() - lastStarted(deps.environment) < DAY_MS) return false;
|
|
3112
|
+
function write(environment, marks) {
|
|
3030
3113
|
try {
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
{ mode: 384 }
|
|
3036
|
-
);
|
|
3037
|
-
(deps.run ?? detached)("npx", ["-y", HOOK_SPECIFIER, "install", "--json"]);
|
|
3038
|
-
return true;
|
|
3114
|
+
mkdirSync6(configHome(environment), { recursive: true, mode: 448 });
|
|
3115
|
+
writeFileSync4(join7(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
|
|
3116
|
+
mode: 384
|
|
3117
|
+
});
|
|
3039
3118
|
} catch {
|
|
3040
|
-
return false;
|
|
3041
3119
|
}
|
|
3042
3120
|
}
|
|
3121
|
+
function noteHookFired(environment, host, now = Date.now()) {
|
|
3122
|
+
const marks = read(environment);
|
|
3123
|
+
const mark = marks[host];
|
|
3124
|
+
if (mark?.firedAt !== void 0 && mark.firedAt >= (mark.writtenAt ?? 0)) return;
|
|
3125
|
+
write(environment, { ...marks, [host]: { ...mark, firedAt: now } });
|
|
3126
|
+
}
|
|
3043
3127
|
|
|
3044
3128
|
// src/commands/guidance.ts
|
|
3045
3129
|
function readInput(stream) {
|
|
@@ -3107,25 +3191,23 @@ async function runGuidance(options) {
|
|
|
3107
3191
|
event = parsedEvent;
|
|
3108
3192
|
const record = input;
|
|
3109
3193
|
const credentials = readCredentials(options.environment).agents;
|
|
3194
|
+
const repositories = projectScoped ? [] : repositoryHints(options.cwd);
|
|
3110
3195
|
const agents = projectScoped ? credentials.filter(
|
|
3111
3196
|
(agent) => agent.controlPlane === options.controlPlane && agent.repositoryId === options.repositoryId
|
|
3112
3197
|
) : (() => {
|
|
3113
|
-
const selection = selectAgent(
|
|
3114
|
-
credentials,
|
|
3115
|
-
options.controlPlane,
|
|
3116
|
-
void 0,
|
|
3117
|
-
repositoryHints(options.cwd)
|
|
3118
|
-
);
|
|
3198
|
+
const selection = selectAgent(credentials, options.controlPlane, void 0, repositories);
|
|
3119
3199
|
return selection.kind === "refused" ? [] : [selection.agent];
|
|
3120
3200
|
})();
|
|
3121
3201
|
if (agents.length !== 1 || !agents[0]) {
|
|
3122
3202
|
if (projectScoped) throw new Error("guidance_connection_unavailable");
|
|
3123
3203
|
return 0;
|
|
3124
3204
|
}
|
|
3205
|
+
if (options.hook === "codex") noteHookFired(options.environment, "codex");
|
|
3125
3206
|
const loaded = await loadGuidance({
|
|
3126
3207
|
agent: agents[0],
|
|
3127
3208
|
environment: options.environment,
|
|
3128
3209
|
timeoutMs: 750,
|
|
3210
|
+
hook: options.hook,
|
|
3129
3211
|
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
3130
3212
|
});
|
|
3131
3213
|
if (event === "SessionStart")
|
package/dist/guidance.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function loadGuidance(input: {
|
|
|
24
24
|
environment: NodeJS.ProcessEnv;
|
|
25
25
|
fetchImpl?: typeof fetch;
|
|
26
26
|
timeoutMs?: number;
|
|
27
|
+
/** The host whose hook is asking, so the server can tell a laptop where one host never asks. */
|
|
28
|
+
hook?: "codex" | "claude";
|
|
27
29
|
}): Promise<GuidanceLoad>;
|
|
28
30
|
/** Writes context first, then records emission metadata; this never claims model receipt. */
|
|
29
31
|
export declare function recordGuidanceContext(input: {
|
package/dist/guidance.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { noteServerVersion } from "./currency.js";
|
|
2
|
-
import { CLIENT_HEADER, CLIENT_HEADER_VALUE } from "./wire.js";
|
|
2
|
+
import { CLIENT_HEADER, CLIENT_HEADER_VALUE, HOOK_HOST_HEADER, UPDATE_STATE_HEADER, } from "./wire.js";
|
|
3
|
+
import { selfUpdateState } from "./self-update.js";
|
|
3
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
5
|
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -168,6 +169,7 @@ export async function loadGuidance(input) {
|
|
|
168
169
|
/* Cache is optional. */
|
|
169
170
|
}
|
|
170
171
|
const previous = directory ? cached(directory, scopeKey, input.agent) : undefined;
|
|
172
|
+
const updateState = selfUpdateState(input.environment);
|
|
171
173
|
const abort = new AbortController();
|
|
172
174
|
let timer;
|
|
173
175
|
try {
|
|
@@ -179,6 +181,10 @@ export async function loadGuidance(input) {
|
|
|
179
181
|
headers: {
|
|
180
182
|
authorization: `Bearer ${input.agent.token}`,
|
|
181
183
|
[CLIENT_HEADER]: CLIENT_HEADER_VALUE,
|
|
184
|
+
// How the last background update ended, so a laptop that cannot
|
|
185
|
+
// update itself is seen on the scorecard rather than guessed at.
|
|
186
|
+
...(updateState === undefined ? {} : { [UPDATE_STATE_HEADER]: updateState }),
|
|
187
|
+
...(input.hook === undefined ? {} : { [HOOK_HOST_HEADER]: input.hook }),
|
|
182
188
|
accept: "application/json",
|
|
183
189
|
...(previous ? { "if-none-match": previous.etag } : {}),
|
|
184
190
|
},
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowing when a host has not run the hooks this command last wrote, so the
|
|
3
|
+
* person is told to approve them whenever that is true, not only at setup.
|
|
4
|
+
*
|
|
5
|
+
* Robert, 29 September 2026: hooks will change from release to release, and
|
|
6
|
+
* most releases arrive through the background update, which says nothing to
|
|
7
|
+
* anybody. Codex trusts a hook by the hash of its definition and skips a new
|
|
8
|
+
* or changed one until the person trusts it under /hooks, so a laptop can
|
|
9
|
+
* update itself into silence. Two small marks settle it without reading
|
|
10
|
+
* anything of the host's: when this command last wrote a host's hooks, and
|
|
11
|
+
* when one of that host's hooks last ran. Written after fired means the
|
|
12
|
+
* current hooks have never run, and the host's own session is where the
|
|
13
|
+
* person hears it.
|
|
14
|
+
*/
|
|
15
|
+
export type TrustingHost = "codex";
|
|
16
|
+
/** Hosts that run a hook only after the person has approved it. */
|
|
17
|
+
export declare const HOSTS_REQUIRING_TRUST: readonly TrustingHost[];
|
|
18
|
+
/**
|
|
19
|
+
* This command wrote or changed the host's hooks, or found them in place with
|
|
20
|
+
* no record that it ever had: either way, from now the host has to have run
|
|
21
|
+
* them once before anybody assumes it does.
|
|
22
|
+
*/
|
|
23
|
+
export declare function noteHooksWritten(environment: NodeJS.ProcessEnv, host: TrustingHost, changed: boolean, now?: number): void;
|
|
24
|
+
/** One of the host's hooks ran, which it does only once they are trusted. */
|
|
25
|
+
export declare function noteHookFired(environment: NodeJS.ProcessEnv, host: TrustingHost, now?: number): void;
|
|
26
|
+
/** True while the hooks last written for this host have never run in it. */
|
|
27
|
+
export declare function hooksAwaitingTrust(environment: NodeJS.ProcessEnv, host: TrustingHost): boolean;
|
|
28
|
+
/** The host a session belongs to, from the name it gave when it said hello. */
|
|
29
|
+
export declare function trustingHost(clientName: unknown): TrustingHost | undefined;
|
|
30
|
+
/** What the agent is asked to pass on, once, in the host where the approval happens. */
|
|
31
|
+
export declare const HOOKS_AWAITING_TRUST_NOTICE = "Note from Balladeer on this laptop, to pass on to the person once: Balladeer's hooks here were added or changed and Codex has not run them. Codex runs a hook only after the person trusts it. Ask them to type /hooks in Codex and trust Balladeer's three hooks; until then Codex has Balladeer's tools without its session-start guidance or its updates.";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { configHome } from "./store.js";
|
|
4
|
+
/** Hosts that run a hook only after the person has approved it. */
|
|
5
|
+
export const HOSTS_REQUIRING_TRUST = ["codex"];
|
|
6
|
+
const FILE = "hook-trust.json";
|
|
7
|
+
function read(environment) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(readFileSync(join(configHome(environment), FILE), "utf8"));
|
|
10
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
11
|
+
? parsed
|
|
12
|
+
: {};
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function write(environment, marks) {
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(configHome(environment), { recursive: true, mode: 0o700 });
|
|
21
|
+
writeFileSync(join(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
|
|
22
|
+
mode: 0o600,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* A mark that cannot be written costs a notice, never a session. */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* This command wrote or changed the host's hooks, or found them in place with
|
|
31
|
+
* no record that it ever had: either way, from now the host has to have run
|
|
32
|
+
* them once before anybody assumes it does.
|
|
33
|
+
*/
|
|
34
|
+
export function noteHooksWritten(environment, host, changed, now = Date.now()) {
|
|
35
|
+
const marks = read(environment);
|
|
36
|
+
if (!changed && marks[host]?.writtenAt !== undefined)
|
|
37
|
+
return;
|
|
38
|
+
write(environment, { ...marks, [host]: { ...marks[host], writtenAt: now } });
|
|
39
|
+
}
|
|
40
|
+
/** One of the host's hooks ran, which it does only once they are trusted. */
|
|
41
|
+
export function noteHookFired(environment, host, now = Date.now()) {
|
|
42
|
+
const marks = read(environment);
|
|
43
|
+
const mark = marks[host];
|
|
44
|
+
// Nothing to settle, or already settled: no write on the session's path.
|
|
45
|
+
if (mark?.firedAt !== undefined && mark.firedAt >= (mark.writtenAt ?? 0))
|
|
46
|
+
return;
|
|
47
|
+
write(environment, { ...marks, [host]: { ...mark, firedAt: now } });
|
|
48
|
+
}
|
|
49
|
+
/** True while the hooks last written for this host have never run in it. */
|
|
50
|
+
export function hooksAwaitingTrust(environment, host) {
|
|
51
|
+
const mark = read(environment)[host];
|
|
52
|
+
if (mark?.writtenAt === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
return mark.firedAt === undefined || mark.firedAt < mark.writtenAt;
|
|
55
|
+
}
|
|
56
|
+
/** The host a session belongs to, from the name it gave when it said hello. */
|
|
57
|
+
export function trustingHost(clientName) {
|
|
58
|
+
return typeof clientName === "string" && clientName.toLowerCase().startsWith("codex")
|
|
59
|
+
? "codex"
|
|
60
|
+
: undefined;
|
|
61
|
+
}
|
|
62
|
+
/** What the agent is asked to pass on, once, in the host where the approval happens. */
|
|
63
|
+
export const HOOKS_AWAITING_TRUST_NOTICE = "Note from Balladeer on this laptop, to pass on to the person once: Balladeer's hooks here were added or changed and Codex has not run them. Codex runs a hook only after the person trusts it. Ask them to type /hooks in Codex and trust Balladeer's three hooks; until then Codex has Balladeer's tools without its session-start guidance or its updates.";
|
package/dist/self-update.d.ts
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
|
+
/** Set on the child so the install it runs can record how it ended. */
|
|
2
|
+
export declare const SELF_UPDATE_ENVIRONMENT = "BALLADEER_SELF_UPDATE";
|
|
3
|
+
export type Launch = Readonly<{
|
|
4
|
+
command: string;
|
|
5
|
+
args: readonly string[];
|
|
6
|
+
environment: NodeJS.ProcessEnv;
|
|
7
|
+
}>;
|
|
1
8
|
export type SelfUpdateDeps = Readonly<{
|
|
2
9
|
environment: NodeJS.ProcessEnv;
|
|
3
10
|
now?: () => number;
|
|
4
|
-
|
|
11
|
+
/** Starts the child and answers whether one exists. Absent a return value, it started. */
|
|
12
|
+
run?: (command: string, args: readonly string[], environment: NodeJS.ProcessEnv) => boolean | void;
|
|
13
|
+
/** The node binary this process runs under; the launcher is found beside it. */
|
|
14
|
+
execPath?: string;
|
|
15
|
+
exists?: (path: string) => boolean;
|
|
5
16
|
}>;
|
|
17
|
+
/** A short, closed vocabulary: it travels in a header and lands in a column. */
|
|
18
|
+
export type SelfUpdateState = `started:${string}` | `unstartable:${string}` | `installed:${string}` | `failed:${string}`;
|
|
19
|
+
/** What the last attempt came to, for the next hook fire to report. Never throws. */
|
|
20
|
+
export declare function recordSelfUpdateState(environment: NodeJS.ProcessEnv, state: SelfUpdateState, at: number): void;
|
|
21
|
+
/** The recorded state, or nothing when none was written or it is not one of ours. */
|
|
22
|
+
export declare function selfUpdateState(environment: NodeJS.ProcessEnv): SelfUpdateState | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* How to start the installer without trusting PATH. npm ships npx as a script
|
|
25
|
+
* beside the node it belongs to, so the node this hook runs under can run it
|
|
26
|
+
* directly; that node certainly exists, which is what makes the launch
|
|
27
|
+
* dependable. The shim beside node is second, and PATH is last, for the
|
|
28
|
+
* layouts that keep npm somewhere else.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveLaunch(specifier: string, environment: NodeJS.ProcessEnv, execPath?: string, exists?: (path: string) => boolean): Launch;
|
|
6
31
|
/**
|
|
7
32
|
* Start the update when the server named a newer release and none was
|
|
8
33
|
* started today. Answers whether one was started, for the caller's own
|
|
9
|
-
* record; the session hears nothing either way.
|
|
34
|
+
* record; the session hears nothing either way. A launch that could not start
|
|
35
|
+
* leaves no stamp, so the next session start tries again rather than waiting
|
|
36
|
+
* a day for the same failure.
|
|
10
37
|
*/
|
|
11
38
|
export declare function startSelfUpdateIfDue(newer: string | undefined, deps: SelfUpdateDeps): boolean;
|