@gatebolt/cli 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/dist/agents/claude-code.js +10 -3
- package/dist/commands/declare.js +1 -0
- package/dist/commands/hook.js +66 -4
- package/dist/git.js +7 -0
- package/dist/session.js +2 -1
- package/package.json +1 -1
|
@@ -5,10 +5,12 @@ const CLAUDE_DIR = ".claude";
|
|
|
5
5
|
const SETTINGS_FILE = "settings.json";
|
|
6
6
|
const PRE_TOOL_USE = "PreToolUse";
|
|
7
7
|
const STOP = "Stop";
|
|
8
|
+
const USER_PROMPT_SUBMIT = "UserPromptSubmit";
|
|
8
9
|
const EDIT_TOOLS_MATCHER = "Edit|Write|NotebookEdit";
|
|
9
10
|
const COMMAND_TYPE = "command";
|
|
10
11
|
const GUARD_SUB = "guard";
|
|
11
12
|
const POST_TURN_SUB = "post-turn";
|
|
13
|
+
const PROMPT_SUB = "prompt";
|
|
12
14
|
const ENFORCE_STRICT = "strict";
|
|
13
15
|
const ENFORCE_LENIENT = "lenient";
|
|
14
16
|
const DENY_DECISION = "deny";
|
|
@@ -28,8 +30,8 @@ function guardCommand(cli) {
|
|
|
28
30
|
`{ echo "${MISSING_CLI_MESSAGE}" >&2; ` +
|
|
29
31
|
`printf '%s\\n' '${missingCliDenyPayload()}'; }; exit 0; }`);
|
|
30
32
|
}
|
|
31
|
-
function
|
|
32
|
-
return (`command -v ${cli} >/dev/null 2>&1 && exec ${cli} hook ${
|
|
33
|
+
function advisoryCommand(cli, sub) {
|
|
34
|
+
return (`command -v ${cli} >/dev/null 2>&1 && exec ${cli} hook ${sub} || ` +
|
|
33
35
|
`echo "${MISSING_CLI_MESSAGE}" >&2`);
|
|
34
36
|
}
|
|
35
37
|
export function mergeSettings(existing, opts) {
|
|
@@ -43,10 +45,15 @@ export function mergeSettings(existing, opts) {
|
|
|
43
45
|
}
|
|
44
46
|
hooks[PRE_TOOL_USE] = preToolUse;
|
|
45
47
|
const stop = arrayField(hooks[STOP], `hooks.${STOP}`);
|
|
46
|
-
if (ensureHookCommand(stop, `hook ${POST_TURN_SUB}`,
|
|
48
|
+
if (ensureHookCommand(stop, `hook ${POST_TURN_SUB}`, advisoryCommand(opts.command, POST_TURN_SUB))) {
|
|
47
49
|
changed = true;
|
|
48
50
|
}
|
|
49
51
|
hooks[STOP] = stop;
|
|
52
|
+
const userPrompt = arrayField(hooks[USER_PROMPT_SUBMIT], `hooks.${USER_PROMPT_SUBMIT}`);
|
|
53
|
+
if (ensureHookCommand(userPrompt, `hook ${PROMPT_SUB}`, advisoryCommand(opts.command, PROMPT_SUB))) {
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
hooks[USER_PROMPT_SUBMIT] = userPrompt;
|
|
50
57
|
if (opts.enforce === ENFORCE_STRICT) {
|
|
51
58
|
const env = plainObject(settings.env, "env");
|
|
52
59
|
settings.env = env;
|
package/dist/commands/declare.js
CHANGED
|
@@ -109,6 +109,7 @@ export async function runDeclare(argv) {
|
|
|
109
109
|
baseSha,
|
|
110
110
|
branch,
|
|
111
111
|
profile: connection.profileName,
|
|
112
|
+
sessionId: process.env.CLAUDE_CODE_SESSION_ID,
|
|
112
113
|
});
|
|
113
114
|
process.stdout.write(`Declared change ${result.changeId} (${declaredFiles.length} file(s)).\n`);
|
|
114
115
|
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { formatError } from "../errors.js";
|
|
4
|
-
import { hasCommits, repoContext } from "../git.js";
|
|
4
|
+
import { hasCommits, repoContext, worktreePaths } from "../git.js";
|
|
5
5
|
import { isRecord } from "../json.js";
|
|
6
6
|
import { readSession } from "../session.js";
|
|
7
7
|
import { RequestError } from "../transport.js";
|
|
@@ -23,17 +23,31 @@ task — specific repo-relative paths, not broad globs like src/** :
|
|
|
23
23
|
The GateBolt CLI is already configured for this repository. Once it
|
|
24
24
|
prints "Declared change …", retry the edit.`;
|
|
25
25
|
}
|
|
26
|
+
function promptReminder(cli) {
|
|
27
|
+
return `This repository uses GateBolt strict mode. Before editing any file, declare
|
|
28
|
+
the files you plan to touch — run this in Bash first, listing EVERY file you will
|
|
29
|
+
create or modify for this task:
|
|
30
|
+
|
|
31
|
+
${cli} declare \\
|
|
32
|
+
--task "<one line describing the task>" \\
|
|
33
|
+
--vendor claude_code --name "Claude Code" --model <your-model-id> \\
|
|
34
|
+
--file <path> [--file <path> ...]
|
|
35
|
+
|
|
36
|
+
Then make your edits. Editing before declaring will be blocked.`;
|
|
37
|
+
}
|
|
26
38
|
export async function runHook(argv, input = {}) {
|
|
27
39
|
const sub = argv[0] ?? "";
|
|
28
40
|
switch (sub) {
|
|
29
41
|
case "guard":
|
|
30
42
|
return runGuard(input);
|
|
43
|
+
case "prompt":
|
|
44
|
+
return runPrompt(input);
|
|
31
45
|
case "post-turn":
|
|
32
46
|
return runPostTurn(input);
|
|
33
47
|
case "pre-push":
|
|
34
48
|
return runPrePush(input);
|
|
35
49
|
default:
|
|
36
|
-
throw new Error(`Unknown hook '${sub}'. Expected one of: guard, post-turn, pre-push.`);
|
|
50
|
+
throw new Error(`Unknown hook '${sub}'. Expected one of: guard, prompt, post-turn, pre-push.`);
|
|
37
51
|
}
|
|
38
52
|
}
|
|
39
53
|
async function resolveRepo(input) {
|
|
@@ -63,6 +77,7 @@ export async function readHookPayload() {
|
|
|
63
77
|
filePath: toolInput && typeof toolInput.file_path === "string"
|
|
64
78
|
? toolInput.file_path
|
|
65
79
|
: undefined,
|
|
80
|
+
sessionId: typeof data.session_id === "string" ? data.session_id : undefined,
|
|
66
81
|
};
|
|
67
82
|
}
|
|
68
83
|
function safeJsonParse(raw) {
|
|
@@ -108,12 +123,36 @@ async function runGuard(input) {
|
|
|
108
123
|
const cli = process.env.GATEBOLT_CLI ?? DEFAULT_CLI;
|
|
109
124
|
process.stdout.write(denyPayload(declareInstructions(cli)));
|
|
110
125
|
}
|
|
126
|
+
// Proactively remind the agent to declare before its first edit, so it doesn't
|
|
127
|
+
// discover strict mode by tripping the guard. Silent once a declaration is open.
|
|
128
|
+
async function runPrompt(input) {
|
|
129
|
+
if (process.env.GATEBOLT_ENFORCE !== ENFORCE_STRICT)
|
|
130
|
+
return;
|
|
131
|
+
const repo = await resolveRepo(input);
|
|
132
|
+
if (!repo || !(await hasCommits()))
|
|
133
|
+
return;
|
|
134
|
+
if (await readSession(repo.gitDir))
|
|
135
|
+
return;
|
|
136
|
+
process.stdout.write(promptReminder(process.env.GATEBOLT_CLI ?? DEFAULT_CLI));
|
|
137
|
+
}
|
|
111
138
|
async function runPostTurn(input) {
|
|
112
139
|
const repo = await resolveRepo(input);
|
|
113
140
|
if (!repo)
|
|
114
141
|
return;
|
|
115
|
-
if (
|
|
116
|
-
return;
|
|
142
|
+
if (await readSession(repo.gitDir))
|
|
143
|
+
return observeHere();
|
|
144
|
+
for (const { path, session } of await worktreeSessions()) {
|
|
145
|
+
if (session.sessionId === undefined) {
|
|
146
|
+
process.stderr.write(`GateBolt: ${path} has an open declaration from outside this session; leaving it open.\n`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (session.sessionId !== input.sessionId)
|
|
150
|
+
continue;
|
|
151
|
+
if (chdirTo(path))
|
|
152
|
+
await observeHere();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function observeHere() {
|
|
117
156
|
try {
|
|
118
157
|
await runObserve([]);
|
|
119
158
|
}
|
|
@@ -121,6 +160,29 @@ async function runPostTurn(input) {
|
|
|
121
160
|
process.stderr.write(`${formatError(error)}\n`);
|
|
122
161
|
}
|
|
123
162
|
}
|
|
163
|
+
async function worktreeSessions() {
|
|
164
|
+
const found = [];
|
|
165
|
+
for (const path of await worktreePaths().catch(() => [])) {
|
|
166
|
+
if (!chdirTo(path))
|
|
167
|
+
continue;
|
|
168
|
+
const repo = await hookRepoContext();
|
|
169
|
+
const session = repo ? await readSession(repo.gitDir) : null;
|
|
170
|
+
if (session)
|
|
171
|
+
found.push({ path, session });
|
|
172
|
+
}
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
// `git worktree list` keeps reporting a worktree whose directory was deleted
|
|
176
|
+
// without a `git worktree prune`.
|
|
177
|
+
function chdirTo(path) {
|
|
178
|
+
try {
|
|
179
|
+
process.chdir(path);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
124
186
|
// Backstop for the interrupt case the Stop hook misses: an open, un-observed
|
|
125
187
|
// declaration.
|
|
126
188
|
async function runPrePush(input) {
|
package/dist/git.js
CHANGED
|
@@ -34,6 +34,13 @@ export async function hooksPath() {
|
|
|
34
34
|
const output = await git(["rev-parse", "--git-path", "hooks"]);
|
|
35
35
|
return resolve(output.trim());
|
|
36
36
|
}
|
|
37
|
+
export async function worktreePaths() {
|
|
38
|
+
const output = await git(["worktree", "list", "--porcelain"]);
|
|
39
|
+
return output
|
|
40
|
+
.split("\n")
|
|
41
|
+
.filter((line) => line.startsWith("worktree "))
|
|
42
|
+
.map((line) => line.replace(/^worktree /, "").trim());
|
|
43
|
+
}
|
|
37
44
|
export async function isPathIgnored(path) {
|
|
38
45
|
try {
|
|
39
46
|
await git(["check-ignore", "--quiet", path]);
|
package/dist/session.js
CHANGED
|
@@ -26,12 +26,13 @@ export function writeSession(gitDir, session) {
|
|
|
26
26
|
return writeJson(gitDir, SESSION_FILE, session);
|
|
27
27
|
}
|
|
28
28
|
export function readSession(gitDir) {
|
|
29
|
-
return readJson(gitDir, SESSION_FILE, ({ changeId, baseSha, branch, profile }) => typeof changeId === "string" && typeof baseSha === "string"
|
|
29
|
+
return readJson(gitDir, SESSION_FILE, ({ changeId, baseSha, branch, profile, sessionId }) => typeof changeId === "string" && typeof baseSha === "string"
|
|
30
30
|
? {
|
|
31
31
|
changeId,
|
|
32
32
|
baseSha,
|
|
33
33
|
branch: typeof branch === "string" ? branch : undefined,
|
|
34
34
|
profile: typeof profile === "string" ? profile : undefined,
|
|
35
|
+
sessionId: typeof sessionId === "string" ? sessionId : undefined,
|
|
35
36
|
}
|
|
36
37
|
: null);
|
|
37
38
|
}
|
package/package.json
CHANGED