@basou/cli 0.49.0 → 0.50.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/dist/index.js +162 -31
- package/dist/index.js.map +1 -1
- package/dist/program.js +162 -31
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2474,6 +2474,9 @@ import {
|
|
|
2474
2474
|
evaluateStopHook,
|
|
2475
2475
|
findBasouSessionStartHook,
|
|
2476
2476
|
findBasouStopHookCommand,
|
|
2477
|
+
findClaudeSessionStartHooks,
|
|
2478
|
+
findUnrecognizedSessionStart,
|
|
2479
|
+
isClaudeSessionStartMalformed,
|
|
2477
2480
|
isProtocolUpdateDue,
|
|
2478
2481
|
ORIENTATION_END as ORIENTATION_END2,
|
|
2479
2482
|
ORIENTATION_START as ORIENTATION_START2,
|
|
@@ -2488,10 +2491,12 @@ import {
|
|
|
2488
2491
|
readManifest as readManifest5,
|
|
2489
2492
|
readMarkdownFile as readMarkdownFile6,
|
|
2490
2493
|
recordSessionBaseline,
|
|
2494
|
+
removeClaudeSessionStartHook,
|
|
2491
2495
|
removeSessionStartHook,
|
|
2492
2496
|
removeStopHook,
|
|
2493
2497
|
renderProtocolUpdate,
|
|
2494
2498
|
transcriptStartedAt,
|
|
2499
|
+
upsertClaudeSessionStartHook,
|
|
2495
2500
|
upsertSessionStartHook,
|
|
2496
2501
|
upsertStopHook
|
|
2497
2502
|
} from "@basou/core";
|
|
@@ -3842,11 +3847,14 @@ function registerHookCommand(program2) {
|
|
|
3842
3847
|
});
|
|
3843
3848
|
});
|
|
3844
3849
|
hook.command("install [target]").description(
|
|
3845
|
-
"Register a basou hook (reproducible, idempotent). Target `claude` (default): the Stop hook in ~/.claude/settings.json
|
|
3850
|
+
"Register a basou hook (reproducible, idempotent). Target `claude` (default): the Stop hook and the SessionStart hook in ~/.claude/settings.json. The Stop hook is advisory capture-only by default; --block opts into in-turn enforcement, --require-review into the review gate. The SessionStart hook hands each session the workspace's position and records the git baseline the Stop hook measures the session's file changes against; --no-session-start leaves it out. Target `codex`: the SessionStart hook in ~/.codex/hooks.json, which hands each Codex session the position of the workspace it was opened in. Codex asks you to review and trust a new hook once before it runs."
|
|
3846
3851
|
).option(
|
|
3847
3852
|
"--block",
|
|
3848
3853
|
"claude: register the blocking (opt-in enforcement) form instead of advisory"
|
|
3849
|
-
).option("--require-review", "claude: register with the opt-in review gate enabled").option("--min-edits <n>", "claude: pass a custom file-edit threshold to the registered hook").option(
|
|
3854
|
+
).option("--require-review", "claude: register with the opt-in review gate enabled").option("--min-edits <n>", "claude: pass a custom file-edit threshold to the registered hook").option(
|
|
3855
|
+
"--no-session-start",
|
|
3856
|
+
"claude: register the Stop hook only, and leave any SessionStart hook as it is"
|
|
3857
|
+
).option("--settings <path>", "claude: override the settings.json path (intended for tests)").option("--hooks <path>", "codex: override the hooks.json path (intended for tests)").option(
|
|
3850
3858
|
"--codex-config <path>",
|
|
3851
3859
|
"codex: override the Codex config.toml path (intended for tests)"
|
|
3852
3860
|
).option(
|
|
@@ -3855,11 +3863,23 @@ function registerHookCommand(program2) {
|
|
|
3855
3863
|
).option("--dry-run", "Print what would change without writing").option("-v, --verbose", "Show error causes").action(async (target, opts) => {
|
|
3856
3864
|
await dispatchHookTarget(target, opts, {
|
|
3857
3865
|
claude: () => runHookInstall(opts),
|
|
3858
|
-
codex: () =>
|
|
3866
|
+
codex: async () => {
|
|
3867
|
+
if (opts.sessionStart === false) {
|
|
3868
|
+
renderCliError(
|
|
3869
|
+
new Error(
|
|
3870
|
+
"--no-session-start applies to target claude only; the Codex hook is itself a SessionStart hook."
|
|
3871
|
+
),
|
|
3872
|
+
{ verbose: isVerbose(opts) }
|
|
3873
|
+
);
|
|
3874
|
+
process.exitCode = 1;
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
await runCodexHookInstall(opts);
|
|
3878
|
+
}
|
|
3859
3879
|
});
|
|
3860
3880
|
});
|
|
3861
3881
|
hook.command("uninstall [target]").description(
|
|
3862
|
-
"Remove a basou hook, leaving other hooks intact. Target `claude` (default): the Stop hook in ~/.claude/settings.json. Target `codex`: the SessionStart hook in ~/.codex/hooks.json."
|
|
3882
|
+
"Remove a basou hook, leaving other hooks intact. Target `claude` (default): the Stop hook and the SessionStart hook in ~/.claude/settings.json. Target `codex`: the SessionStart hook in ~/.codex/hooks.json."
|
|
3863
3883
|
).option("--settings <path>", "claude: override the settings.json path (intended for tests)").option("--hooks <path>", "codex: override the hooks.json path (intended for tests)").option("--dry-run", "Print what would change without writing").option("-v, --verbose", "Show error causes").action(async (target, opts) => {
|
|
3864
3884
|
await dispatchHookTarget(target, opts, {
|
|
3865
3885
|
claude: () => runHookUninstall(opts),
|
|
@@ -3867,7 +3887,7 @@ function registerHookCommand(program2) {
|
|
|
3867
3887
|
});
|
|
3868
3888
|
});
|
|
3869
3889
|
hook.command("status [target]").description(
|
|
3870
|
-
"Report whether a basou hook is registered. Target `claude` (default): the Stop hook and its mode. Target `codex`: the SessionStart hook, and whether Codex has trusted it yet."
|
|
3890
|
+
"Report whether a basou hook is registered. Target `claude` (default): the Stop hook and its mode, and the SessionStart hook. Target `codex`: the SessionStart hook, and whether Codex has trusted it yet."
|
|
3871
3891
|
).option("--settings <path>", "claude: override the settings.json path (intended for tests)").option("--hooks <path>", "codex: override the hooks.json path (intended for tests)").option(
|
|
3872
3892
|
"--codex-config <path>",
|
|
3873
3893
|
"codex: override the Codex config.toml path (intended for tests)"
|
|
@@ -3915,9 +3935,11 @@ becomes the session's trusted context, so it withholds the position instead.
|
|
|
3915
3935
|
Run 'basou refresh' to see which lines are responsible.
|
|
3916
3936
|
|
|
3917
3937
|
Claude Code's SessionStart hook sends the same kind of payload (a JSON object
|
|
3918
|
-
with 'cwd') and adds stdout to context the same way
|
|
3919
|
-
|
|
3920
|
-
|
|
3938
|
+
with 'cwd') and adds stdout to context the same way. 'basou hook install'
|
|
3939
|
+
registers this command there too, in ~/.claude/settings.json, and replaces a
|
|
3940
|
+
hand-registered 'basou orient' SessionStart hook with it. In Claude Code it also
|
|
3941
|
+
records where each declared repository stood when the session started, which is
|
|
3942
|
+
what the Stop hook measures the session's file changes against.
|
|
3921
3943
|
|
|
3922
3944
|
Codex trusts hooks by hash. A newly installed or changed hook is skipped until
|
|
3923
3945
|
you review it: the interactive CLI asks at startup ("Hooks need review"), the
|
|
@@ -4231,6 +4253,7 @@ function normalizeInstallOptions(raw) {
|
|
|
4231
4253
|
const out = {};
|
|
4232
4254
|
if (raw.block === true) out.block = true;
|
|
4233
4255
|
if (raw.requireReview === true) out.requireReview = true;
|
|
4256
|
+
if (raw.sessionStart === false) out.noSessionStart = true;
|
|
4234
4257
|
if (raw.settings !== void 0) out.settings = raw.settings;
|
|
4235
4258
|
if (raw.hooks !== void 0) out.hooks = raw.hooks;
|
|
4236
4259
|
if (raw.codexConfig !== void 0) out.codexConfig = raw.codexConfig;
|
|
@@ -4302,15 +4325,32 @@ async function doRunHookInstall(options, ctx = {}) {
|
|
|
4302
4325
|
});
|
|
4303
4326
|
await assertNotSymlink(settingsPath);
|
|
4304
4327
|
const { raw, parsed } = await readSettings(settingsPath);
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4328
|
+
const stop = upsertStopHook(parsed, command);
|
|
4329
|
+
let sessionStart;
|
|
4330
|
+
if (options.noSessionStart === true) {
|
|
4331
|
+
sessionStart = { kind: "skipped" };
|
|
4332
|
+
} else {
|
|
4333
|
+
try {
|
|
4334
|
+
const upsert = upsertClaudeSessionStartHook(
|
|
4335
|
+
stop.settings,
|
|
4336
|
+
buildSessionStartHookCommand({ cliEntry })
|
|
4337
|
+
);
|
|
4338
|
+
sessionStart = { kind: "done", action: upsert.action, settings: upsert.settings };
|
|
4339
|
+
} catch (error) {
|
|
4340
|
+
if (!isClaudeSessionStartMalformed(stop.settings)) throw error;
|
|
4341
|
+
sessionStart = { kind: "malformed" };
|
|
4342
|
+
}
|
|
4311
4343
|
}
|
|
4312
|
-
|
|
4313
|
-
|
|
4344
|
+
const settings = sessionStart.kind === "done" ? sessionStart.settings : stop.settings;
|
|
4345
|
+
const changed = stop.action !== "unchanged" || sessionStart.kind === "done" && sessionStart.action !== "unchanged";
|
|
4346
|
+
const dryRun = options.dryRun === true;
|
|
4347
|
+
const lines = [
|
|
4348
|
+
describeStopInstall(stop.action, mode, dryRun),
|
|
4349
|
+
describeSessionStartInstall(sessionStart, dryRun),
|
|
4350
|
+
...describeSessionStartLeftovers(settings)
|
|
4351
|
+
];
|
|
4352
|
+
if (!changed || dryRun) {
|
|
4353
|
+
for (const line of lines) console.log(line);
|
|
4314
4354
|
return;
|
|
4315
4355
|
}
|
|
4316
4356
|
const recheck = await readSettings(settingsPath);
|
|
@@ -4320,8 +4360,55 @@ async function doRunHookInstall(options, ctx = {}) {
|
|
|
4320
4360
|
);
|
|
4321
4361
|
}
|
|
4322
4362
|
await backupSettingsOnce(settingsPath, raw);
|
|
4323
|
-
await writeFileDurable(settingsPath,
|
|
4324
|
-
|
|
4363
|
+
await writeFileDurable(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
4364
|
+
`);
|
|
4365
|
+
for (const line of lines) console.log(line);
|
|
4366
|
+
}
|
|
4367
|
+
function describeStopInstall(action, mode, dryRun) {
|
|
4368
|
+
if (action === "unchanged") {
|
|
4369
|
+
return `The basou Stop hook is already registered (${mode}); no change.`;
|
|
4370
|
+
}
|
|
4371
|
+
if (dryRun) {
|
|
4372
|
+
return `[dry-run] Would ${action === "installed" ? "install" : "update"} the basou Stop hook (${mode}).`;
|
|
4373
|
+
}
|
|
4374
|
+
return `${action === "installed" ? "Installed" : "Updated"} the basou Stop hook (${mode}).`;
|
|
4375
|
+
}
|
|
4376
|
+
function describeSessionStartInstall(outcome, dryRun) {
|
|
4377
|
+
if (outcome.kind === "skipped") {
|
|
4378
|
+
return "The SessionStart hook was left as it is (--no-session-start).";
|
|
4379
|
+
}
|
|
4380
|
+
if (outcome.kind === "malformed") {
|
|
4381
|
+
return "The basou SessionStart hook was NOT installed: 'hooks.SessionStart' in settings.json is not a list of hook groups. Fix it and re-run 'basou hook install'.";
|
|
4382
|
+
}
|
|
4383
|
+
const action = outcome.action;
|
|
4384
|
+
if (action === "unchanged") {
|
|
4385
|
+
return "The basou SessionStart hook is already registered; no change.";
|
|
4386
|
+
}
|
|
4387
|
+
const prefix = dryRun ? "[dry-run] Would " : "";
|
|
4388
|
+
if (action === "replaced") {
|
|
4389
|
+
return `${prefix}${dryRun ? "replace" : "Replaced"} the hand-registered 'basou orient' SessionStart hook with 'basou hook session-start': the same position, plus the baseline the Stop hook measures the session's file changes against. It differs in three ways: it speaks only for a workspace registered in ~/.basou/portfolio.yaml, it does not rewrite .basou/orientation.md at session start, and it stays silent when the position names another registered workspace.`;
|
|
4390
|
+
}
|
|
4391
|
+
if (action === "updated") {
|
|
4392
|
+
return `${prefix}${dryRun ? "update" : "Updated"} the basou SessionStart hook.`;
|
|
4393
|
+
}
|
|
4394
|
+
return `${prefix}${dryRun ? "install" : "Installed"} the basou SessionStart hook: each session gets the workspace's position, and the Stop hook can see the files the session changes through the shell.`;
|
|
4395
|
+
}
|
|
4396
|
+
function describeSessionStartLeftovers(settings) {
|
|
4397
|
+
const own = findClaudeSessionStartHooks(settings);
|
|
4398
|
+
if (own.length === 0) return [];
|
|
4399
|
+
const lines = [];
|
|
4400
|
+
for (const other of findUnrecognizedSessionStart(settings)) {
|
|
4401
|
+
lines.push(
|
|
4402
|
+
`note: this SessionStart hook runs basou '${other.runs === "orient" ? "orient" : "hook session-start"}' inside a longer command, so it was left as written, and a session may receive the position twice: ${other.command}`
|
|
4403
|
+
);
|
|
4404
|
+
}
|
|
4405
|
+
if (own.length > 1) {
|
|
4406
|
+
const matchers = own.map((o) => o.matcher ?? "(every source)").join(", ");
|
|
4407
|
+
lines.push(
|
|
4408
|
+
`note: ${own.length} basou SessionStart hooks remain, under different matchers (${matchers}). They were kept so that no session source stops firing; a source that more than one matches receives the position twice. Remove the one you do not want by hand.`
|
|
4409
|
+
);
|
|
4410
|
+
}
|
|
4411
|
+
return lines;
|
|
4325
4412
|
}
|
|
4326
4413
|
async function runHookUninstall(options) {
|
|
4327
4414
|
try {
|
|
@@ -4339,15 +4426,22 @@ async function doRunHookUninstall(options) {
|
|
|
4339
4426
|
console.log("No settings.json; nothing to remove.");
|
|
4340
4427
|
return;
|
|
4341
4428
|
}
|
|
4342
|
-
const
|
|
4343
|
-
|
|
4344
|
-
|
|
4429
|
+
const stop = removeStopHook(parsed);
|
|
4430
|
+
const sessionStart = removeClaudeSessionStartHook(stop.settings);
|
|
4431
|
+
if (stop.action === "absent" && sessionStart.action === "absent") {
|
|
4432
|
+
console.log("No basou Stop hook or SessionStart hook found; nothing removed.");
|
|
4345
4433
|
return;
|
|
4346
4434
|
}
|
|
4347
|
-
const newBody = `${JSON.stringify(settings, null, 2)}
|
|
4435
|
+
const newBody = `${JSON.stringify(sessionStart.settings, null, 2)}
|
|
4348
4436
|
`;
|
|
4437
|
+
const removedNames = [
|
|
4438
|
+
...stop.action === "removed" ? ["Stop hook"] : [],
|
|
4439
|
+
...sessionStart.action === "removed" ? ["SessionStart hook"] : []
|
|
4440
|
+
];
|
|
4349
4441
|
if (options.dryRun === true) {
|
|
4350
|
-
|
|
4442
|
+
for (const name of removedNames) {
|
|
4443
|
+
console.log(`[dry-run] Would remove the basou ${name} from settings.json.`);
|
|
4444
|
+
}
|
|
4351
4445
|
return;
|
|
4352
4446
|
}
|
|
4353
4447
|
const recheck = await readSettings(settingsPath);
|
|
@@ -4358,7 +4452,9 @@ async function doRunHookUninstall(options) {
|
|
|
4358
4452
|
}
|
|
4359
4453
|
await backupSettingsOnce(settingsPath, raw);
|
|
4360
4454
|
await writeFileDurable(settingsPath, newBody);
|
|
4361
|
-
|
|
4455
|
+
for (const name of removedNames) {
|
|
4456
|
+
console.log(`Removed the basou ${name} from settings.json.`);
|
|
4457
|
+
}
|
|
4362
4458
|
}
|
|
4363
4459
|
async function runHookStatus(options) {
|
|
4364
4460
|
try {
|
|
@@ -4374,14 +4470,49 @@ async function doRunHookStatus(options) {
|
|
|
4374
4470
|
const command = findBasouStopHookCommand(parsed);
|
|
4375
4471
|
if (command === null) {
|
|
4376
4472
|
console.log("basou Stop hook: not registered. Run 'basou hook install' to register it.");
|
|
4473
|
+
} else {
|
|
4474
|
+
const mode = describeHookMode({
|
|
4475
|
+
block: / --block\b/.test(command),
|
|
4476
|
+
review: / --require-review\b/.test(command)
|
|
4477
|
+
});
|
|
4478
|
+
console.log(`basou Stop hook: registered, ${mode}.`);
|
|
4479
|
+
await reportHookEntryBuild(command);
|
|
4480
|
+
}
|
|
4481
|
+
reportSessionStartStatus(parsed);
|
|
4482
|
+
}
|
|
4483
|
+
function reportSessionStartStatus(parsed) {
|
|
4484
|
+
if (isClaudeSessionStartMalformed(parsed)) {
|
|
4485
|
+
console.log(
|
|
4486
|
+
"basou SessionStart hook: cannot tell -- 'hooks.SessionStart' in settings.json is not a list of hook groups."
|
|
4487
|
+
);
|
|
4377
4488
|
return;
|
|
4378
4489
|
}
|
|
4379
|
-
const
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4490
|
+
const own = findClaudeSessionStartHooks(parsed);
|
|
4491
|
+
const others = findUnrecognizedSessionStart(parsed);
|
|
4492
|
+
for (const hook of own.filter((o) => o.kind === "session-start")) {
|
|
4493
|
+
console.log(`basou SessionStart hook: registered, fires ${describeFiring(hook.matcher)}.`);
|
|
4494
|
+
}
|
|
4495
|
+
for (const hook of own.filter((o) => o.kind === "orient")) {
|
|
4496
|
+
console.log(
|
|
4497
|
+
`basou SessionStart hook: 'basou orient' registered by hand, fires ${describeFiring(hook.matcher)}. It delivers the position but records no git baseline, so the files a session changes through the shell are not observed; 'basou hook install' replaces it with 'basou hook session-start'.`
|
|
4498
|
+
);
|
|
4499
|
+
}
|
|
4500
|
+
if (own.length === 0) {
|
|
4501
|
+
if (others.length === 0) {
|
|
4502
|
+
console.log(
|
|
4503
|
+
"basou SessionStart hook: not registered. Sessions get no position at start, and the files a session changes through the shell are not observed. Run 'basou hook install' to register it."
|
|
4504
|
+
);
|
|
4505
|
+
}
|
|
4506
|
+
for (const other of others) {
|
|
4507
|
+
console.log(
|
|
4508
|
+
other.runs === "orient" ? `basou SessionStart hook: not registered by basou. A SessionStart hook runs 'basou orient' inside a longer command, so sessions may get the position, but no git baseline is recorded: ${other.command}` : `basou SessionStart hook: not registered by basou. A SessionStart hook runs 'basou hook session-start' inside a longer command, which basou leaves as written: ${other.command}`
|
|
4509
|
+
);
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
for (const line of describeSessionStartLeftovers(parsed)) console.log(line);
|
|
4513
|
+
}
|
|
4514
|
+
function describeFiring(matcher) {
|
|
4515
|
+
return matcher === void 0 || matcher === "" || matcher === "*" ? "on every session source" : `on ${matcher}`;
|
|
4385
4516
|
}
|
|
4386
4517
|
async function reportHookEntryBuild(command) {
|
|
4387
4518
|
console.log(` this basou is: ${BASOU_VERSION_LINE}`);
|
|
@@ -13862,7 +13993,7 @@ async function assertWorkspaceInitialized15(basouRoot) {
|
|
|
13862
13993
|
function readBuildStamp() {
|
|
13863
13994
|
if (false) return void 0;
|
|
13864
13995
|
try {
|
|
13865
|
-
return JSON.parse('{"version":"0.
|
|
13996
|
+
return JSON.parse('{"version":"0.50.0","commit":"5b4c5dc","committedAt":"2026-09-23T19:12:06+09:00"}');
|
|
13866
13997
|
} catch {
|
|
13867
13998
|
return void 0;
|
|
13868
13999
|
}
|