auriga-cli 1.4.1 → 1.5.1
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/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/hooks.d.ts +36 -1
- package/dist/hooks.js +81 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ This repo itself is a fully configured harness project. You can clone it to see
|
|
|
14
14
|
| **Skills** | Development process skills — brainstorming, systematic-debugging, TDD, verification, planning, playwright |
|
|
15
15
|
| **Recommended Skills** | Optional utility skills (e.g. `ui-ux-pro-max`) you can add on top of the workflow skills |
|
|
16
16
|
| **Plugins** | Recommended Claude Code plugins — skill-creator, claude-md-management, codex |
|
|
17
|
-
| **Hooks** | Claude Code hooks
|
|
17
|
+
| **Hooks** | Claude Code hooks: `notify` (macOS notification), `pr-create-guard` (PostToolUse body snapshot after `gh pr create`), `pr-ready-guard` (PreToolUse block on stray planning docs / unpushed commits before `gh pr ready`) |
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
@@ -80,6 +80,8 @@ Installs Claude Code hooks into a chosen scope. Each hook is self-contained unde
|
|
|
80
80
|
| Hook | Description |
|
|
81
81
|
|---|---|
|
|
82
82
|
| notify | Native macOS notification when Claude needs your attention. Shows the brand mark in the small app-icon position, with click-to-activate that brings the originating terminal back to the foreground. Auto-installs `alerter` via Homebrew (`vjeantet/tap/alerter`). Customize sound and icon by editing `.claude/hooks/notify/config.json` and replacing `.claude/hooks/notify/icon.png`. macOS-only at runtime; silent no-op on other platforms. |
|
|
83
|
+
| pr-create-guard | PostToolUse hook for `gh pr create`. Queries the newly-created PR via `gh pr view` and injects a body snapshot (headings found + TODO-checkbox counts) as `additionalContext` for the Agent to self-verify against the step-10 scope / acceptance / risks / TODO contract. Never blocks — PostToolUse runs after the fact. Graceful degradation when gh is unavailable. |
|
|
84
|
+
| pr-ready-guard | PreToolUse hook for `gh pr ready`. Blocks on structural signals only: stray planning docs at `findings.md` / `progress.md` / `task_plan.md` / `docs/superpowers/specs/*.md` (must be archived to `docs/worklog-<date>-<branch>/` per the `Document Conventions` in CLAUDE.md), or unpushed commits on the current branch. No text regex of PR content is ever used as a block signal. On pass, injects a PR body snapshot as `additionalContext`. |
|
|
83
85
|
|
|
84
86
|
Scope choices:
|
|
85
87
|
|
package/README.zh-CN.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| **Skills** | 开发流程 skills —— brainstorming、systematic-debugging、TDD、verification、planning、playwright |
|
|
15
15
|
| **Recommended Skills** | 可选的工具类 skills(如 `ui-ux-pro-max`),在 workflow skills 之外按需追加 |
|
|
16
16
|
| **Plugins** | 推荐的 Claude Code 插件 —— skill-creator、claude-md-management、codex |
|
|
17
|
-
| **Hooks** | Claude Code hooks
|
|
17
|
+
| **Hooks** | Claude Code hooks:`notify`(macOS 通知)、`pr-create-guard`(`gh pr create` 后注入 PR body 快照的 PostToolUse)、`pr-ready-guard`(`gh pr ready` 前按游离 planning 文档 / 未 push commits 拦截的 PreToolUse) |
|
|
18
18
|
|
|
19
19
|
## 快速开始
|
|
20
20
|
|
|
@@ -80,6 +80,8 @@ npx auriga-cli
|
|
|
80
80
|
| Hook | 说明 |
|
|
81
81
|
|---|---|
|
|
82
82
|
| notify | 当 Claude 需要你关注时弹一条原生 macOS 通知。在通知小图标位显示品牌图,点击通知可把发起 Claude 的终端拉回前台。会自动通过 Homebrew 安装 `alerter`(`vjeantet/tap/alerter`)。改 `.claude/hooks/notify/config.json` 即可换提示音、替换 `.claude/hooks/notify/icon.png` 即可换图标。仅 macOS 运行时生效,其它平台静默 no-op。 |
|
|
83
|
+
| pr-create-guard | `gh pr create` 的 PostToolUse hook。创建成功后通过 `gh pr view` 拉真实 PR body,扫 `^##` / `^###` headings 并统计 `- [ ]` / `- [x]`,通过 `additionalContext` 注入快照让 Agent 对照 step 10 的"范围 / 验收标准 / 风险 / 剩余 TODO"四要素。不 block——PostToolUse 发生在动作之后。gh 不可用时静默降级。 |
|
|
84
|
+
| pr-ready-guard | `gh pr ready` 的 PreToolUse hook。**只按结构信号**拦截:仓库根存在游离 planning 文档(`findings.md` / `progress.md` / `task_plan.md`),或 `docs/superpowers/specs/*.md` 未归档——这些必须按 CLAUDE.md 的"文档规范"迁到 `docs/worklog-<date>-<branch>/`;或者本地有未 push commits。**不做 PR 正文文本 regex 匹配**。放行时注入 PR body 快照。 |
|
|
83
85
|
|
|
84
86
|
作用域选择:
|
|
85
87
|
|
package/dist/hooks.d.ts
CHANGED
|
@@ -5,7 +5,17 @@ export interface HookDep {
|
|
|
5
5
|
}
|
|
6
6
|
export interface HookSettingsEvent {
|
|
7
7
|
event: string;
|
|
8
|
+
/** Tool-name / regex filter; mapped onto the container-level
|
|
9
|
+
* `matcher` field in settings.json. Absent → every tool fires. */
|
|
8
10
|
matcher?: string;
|
|
11
|
+
/** Permission-rule-syntax filter — mapped onto the nested action-level
|
|
12
|
+
* `if` field in settings.json. Format: `<ToolName>(<substring>)`, e.g.
|
|
13
|
+
* `Bash(gh pr create)` — see IF_RE for the exact grammar. Lets the
|
|
14
|
+
* Claude Code runtime skip hook dispatch when the tool input doesn't
|
|
15
|
+
* match, avoiding a Node subprocess spawn per unrelated call.
|
|
16
|
+
* Absent → no registry-level content filter (the hook script runs
|
|
17
|
+
* for every invocation that passed `matcher`). */
|
|
18
|
+
if?: string;
|
|
9
19
|
}
|
|
10
20
|
export interface HookDef {
|
|
11
21
|
name: string;
|
|
@@ -32,6 +42,10 @@ export interface SettingsHookAction {
|
|
|
32
42
|
type: "command";
|
|
33
43
|
command: string;
|
|
34
44
|
_marker?: string;
|
|
45
|
+
/** Per-action permission-rule filter (Claude Code ≥ 2026-04 schema).
|
|
46
|
+
* Format: `<ToolName>(<substring>)`. Older runtimes ignore unknown
|
|
47
|
+
* fields, so emitting this is forward-safe. */
|
|
48
|
+
if?: string;
|
|
35
49
|
}
|
|
36
50
|
export interface SettingsHookGroup {
|
|
37
51
|
matcher?: string;
|
|
@@ -52,12 +66,33 @@ export interface SettingsFile {
|
|
|
52
66
|
* and never wrote our marker. Without this fallback we would happily
|
|
53
67
|
* append a duplicate next to it and the hook would fire twice.
|
|
54
68
|
*
|
|
69
|
+
* `options.matcher` writes to the container-level `matcher` (tool-name
|
|
70
|
+
* filter); `options.ifRule` writes to the action-level `if` (permission-
|
|
71
|
+
* rule substring filter, Claude Code ≥ 2026-04). Either or both may be
|
|
72
|
+
* absent.
|
|
73
|
+
*
|
|
74
|
+
* Upgrade path: if an entry with our marker already exists but its
|
|
75
|
+
* matcher / if disagrees with the desired values, we update those two
|
|
76
|
+
* fields in place (preserving everything else — command, sibling
|
|
77
|
+
* actions, the user's other groups — untouched). This is the path for
|
|
78
|
+
* a user who installed an older registry version and re-runs the
|
|
79
|
+
* installer after hooks.json changed. Pure no-op when the existing
|
|
80
|
+
* fields already match.
|
|
81
|
+
*
|
|
82
|
+
* Inputs are defense-in-depth revalidated here against IF_RE + the
|
|
83
|
+
* event-name regex even though registry callers already passed
|
|
84
|
+
* loadHooksConfig, so a direct library caller can't write malformed
|
|
85
|
+
* values into settings.json by bypassing the registry loader.
|
|
86
|
+
*
|
|
55
87
|
* Throws if `settings.hooks[event]` exists but is not an array — that
|
|
56
88
|
* means the user has hand-edited their settings into a shape we do not
|
|
57
89
|
* recognize, and silently replacing it with an empty array would lose
|
|
58
90
|
* data. Callers should catch and surface the error to the user.
|
|
59
91
|
*/
|
|
60
|
-
export declare function addHookToSettings(settings: SettingsFile, event: string, command: string, marker: string
|
|
92
|
+
export declare function addHookToSettings(settings: SettingsFile, event: string, command: string, marker: string, options?: {
|
|
93
|
+
matcher?: string;
|
|
94
|
+
ifRule?: string;
|
|
95
|
+
}): {
|
|
61
96
|
settings: SettingsFile;
|
|
62
97
|
mutated: boolean;
|
|
63
98
|
};
|
package/dist/hooks.js
CHANGED
|
@@ -32,6 +32,29 @@ const EVENT_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
|
32
32
|
// the form requires a code change here, intentionally — see the security
|
|
33
33
|
// review trail in PR #7 for context.
|
|
34
34
|
const COMMAND_RE = /^(node|python3|bash) "\$HOOK_DIR\/[A-Za-z0-9_-]+\.[A-Za-z0-9]+"$/;
|
|
35
|
+
// Claude Code permission-rule syntax for the `if` field:
|
|
36
|
+
// <ToolName>(<substring>)
|
|
37
|
+
// Tool name: EVENT_NAME-shape identifier, bounded to 64 chars so a
|
|
38
|
+
// malicious registry can't inflate settings.json with a gigantic prefix.
|
|
39
|
+
// Substring body: 1-200 printable-ASCII chars, with parens, backslash,
|
|
40
|
+
// and backtick explicitly excluded so the anchored `\(...\)` wrapper
|
|
41
|
+
// actually delimits a well-formed outer parenthesis pair.
|
|
42
|
+
//
|
|
43
|
+
// The safety argument is NOT that IF_RE strips shell metacharacters —
|
|
44
|
+
// `$ " ' ; | & < > *` are all inside the allowed byte range and left
|
|
45
|
+
// intact. The defense is that this string never reaches a shell: it's
|
|
46
|
+
// written verbatim into settings.json as a JSON string and read by
|
|
47
|
+
// Claude Code's in-process permission-rule matcher. A registry
|
|
48
|
+
// compromise can widen or misdirect the match pattern (causing the hook
|
|
49
|
+
// to fire on unintended inputs or not fire at all) but cannot pivot
|
|
50
|
+
// into command execution from this field.
|
|
51
|
+
//
|
|
52
|
+
// Body range decomposition (what the char class actually covers):
|
|
53
|
+
// 0x20-0x27 space through single-quote (excludes nothing)
|
|
54
|
+
// 0x2A-0x5B asterisk through left-bracket (excludes `(` 0x28, `)` 0x29)
|
|
55
|
+
// 0x5D-0x5F right-bracket through underscore (excludes `\` 0x5C)
|
|
56
|
+
// 0x61-0x7E lowercase through tilde (excludes `` ` `` 0x60)
|
|
57
|
+
const IF_RE = /^[A-Z][A-Za-z0-9_-]{0,63}\([\x20-\x27\x2A-\x5B\x5D-\x5F\x61-\x7E]{1,200}\)$/;
|
|
35
58
|
function isSafeRelativePath(file) {
|
|
36
59
|
if (typeof file !== "string" || file.length === 0)
|
|
37
60
|
return false;
|
|
@@ -104,6 +127,10 @@ function validateHookEntry(hook, idx) {
|
|
|
104
127
|
if (matcher !== undefined && (typeof matcher !== "string" || !EVENT_NAME_RE.test(matcher))) {
|
|
105
128
|
throw new Error(`hooks.json: hooks[${idx}].settingsEvents.matcher must match ${EVENT_NAME_RE} (got ${JSON.stringify(matcher)})`);
|
|
106
129
|
}
|
|
130
|
+
const ifRule = evt.if;
|
|
131
|
+
if (ifRule !== undefined && (typeof ifRule !== "string" || !IF_RE.test(ifRule))) {
|
|
132
|
+
throw new Error(`hooks.json: hooks[${idx}].settingsEvents.if must match ${IF_RE} (got ${JSON.stringify(ifRule)})`);
|
|
133
|
+
}
|
|
107
134
|
}
|
|
108
135
|
if (typeof h.command !== "string" || !COMMAND_RE.test(h.command)) {
|
|
109
136
|
throw new Error(`hooks.json: hooks[${idx}].command must match the safe template ${COMMAND_RE} (got ${JSON.stringify(h.command)})`);
|
|
@@ -133,12 +160,40 @@ function validateHookEntry(hook, idx) {
|
|
|
133
160
|
* and never wrote our marker. Without this fallback we would happily
|
|
134
161
|
* append a duplicate next to it and the hook would fire twice.
|
|
135
162
|
*
|
|
163
|
+
* `options.matcher` writes to the container-level `matcher` (tool-name
|
|
164
|
+
* filter); `options.ifRule` writes to the action-level `if` (permission-
|
|
165
|
+
* rule substring filter, Claude Code ≥ 2026-04). Either or both may be
|
|
166
|
+
* absent.
|
|
167
|
+
*
|
|
168
|
+
* Upgrade path: if an entry with our marker already exists but its
|
|
169
|
+
* matcher / if disagrees with the desired values, we update those two
|
|
170
|
+
* fields in place (preserving everything else — command, sibling
|
|
171
|
+
* actions, the user's other groups — untouched). This is the path for
|
|
172
|
+
* a user who installed an older registry version and re-runs the
|
|
173
|
+
* installer after hooks.json changed. Pure no-op when the existing
|
|
174
|
+
* fields already match.
|
|
175
|
+
*
|
|
176
|
+
* Inputs are defense-in-depth revalidated here against IF_RE + the
|
|
177
|
+
* event-name regex even though registry callers already passed
|
|
178
|
+
* loadHooksConfig, so a direct library caller can't write malformed
|
|
179
|
+
* values into settings.json by bypassing the registry loader.
|
|
180
|
+
*
|
|
136
181
|
* Throws if `settings.hooks[event]` exists but is not an array — that
|
|
137
182
|
* means the user has hand-edited their settings into a shape we do not
|
|
138
183
|
* recognize, and silently replacing it with an empty array would lose
|
|
139
184
|
* data. Callers should catch and surface the error to the user.
|
|
140
185
|
*/
|
|
141
|
-
export function addHookToSettings(settings, event, command, marker) {
|
|
186
|
+
export function addHookToSettings(settings, event, command, marker, options = {}) {
|
|
187
|
+
// Defense-in-depth: registry callers pre-validate via loadHooksConfig,
|
|
188
|
+
// but a direct programmatic caller could bypass that. Refuse values
|
|
189
|
+
// that wouldn't have cleared the registry validator, so settings.json
|
|
190
|
+
// can never receive a malformed string through this function.
|
|
191
|
+
if (options.matcher !== undefined && !EVENT_NAME_RE.test(options.matcher)) {
|
|
192
|
+
throw new Error(`addHookToSettings: options.matcher must match ${EVENT_NAME_RE} (got ${JSON.stringify(options.matcher)})`);
|
|
193
|
+
}
|
|
194
|
+
if (options.ifRule !== undefined && !IF_RE.test(options.ifRule)) {
|
|
195
|
+
throw new Error(`addHookToSettings: options.ifRule must match ${IF_RE} (got ${JSON.stringify(options.ifRule)})`);
|
|
196
|
+
}
|
|
142
197
|
const next = JSON.parse(JSON.stringify(settings ?? {}));
|
|
143
198
|
if (next.hooks !== undefined && (typeof next.hooks !== "object" || Array.isArray(next.hooks))) {
|
|
144
199
|
throw new Error(`settings.hooks exists but is not an object; refusing to clobber it`);
|
|
@@ -157,8 +212,21 @@ export function addHookToSettings(settings, event, command, marker) {
|
|
|
157
212
|
if (!action)
|
|
158
213
|
continue;
|
|
159
214
|
if (action._marker === marker) {
|
|
215
|
+
// Our entry already exists. Upgrade matcher / if in place if
|
|
216
|
+
// they drifted from the desired values; leave command + other
|
|
217
|
+
// fields alone (users may have hand-tweaked; we only own the
|
|
218
|
+
// two fields registry declares).
|
|
219
|
+
let drifted = false;
|
|
220
|
+
if (options.matcher !== undefined && group.matcher !== options.matcher) {
|
|
221
|
+
group.matcher = options.matcher;
|
|
222
|
+
drifted = true;
|
|
223
|
+
}
|
|
224
|
+
if (options.ifRule !== undefined && action.if !== options.ifRule) {
|
|
225
|
+
action.if = options.ifRule;
|
|
226
|
+
drifted = true;
|
|
227
|
+
}
|
|
160
228
|
next.hooks[event] = list;
|
|
161
|
-
return { settings: next, mutated:
|
|
229
|
+
return { settings: next, mutated: drifted };
|
|
162
230
|
}
|
|
163
231
|
if (action.type === "command" && action.command === command) {
|
|
164
232
|
// A pre-existing entry (manual or from another tool) already
|
|
@@ -170,9 +238,13 @@ export function addHookToSettings(settings, event, command, marker) {
|
|
|
170
238
|
}
|
|
171
239
|
}
|
|
172
240
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
241
|
+
const action = { type: "command", command, _marker: marker };
|
|
242
|
+
if (options.ifRule !== undefined)
|
|
243
|
+
action.if = options.ifRule;
|
|
244
|
+
const group = { hooks: [action] };
|
|
245
|
+
if (options.matcher !== undefined)
|
|
246
|
+
group.matcher = options.matcher;
|
|
247
|
+
list.push(group);
|
|
176
248
|
next.hooks[event] = list;
|
|
177
249
|
return { settings: next, mutated: true };
|
|
178
250
|
}
|
|
@@ -434,7 +506,10 @@ function writeMergedSettings(resolved, hook, parsed) {
|
|
|
434
506
|
let next = parsed;
|
|
435
507
|
for (const evt of hook.settingsEvents) {
|
|
436
508
|
const cmd = hook.command.replace(/\$HOOK_DIR/g, resolved.commandHookDir);
|
|
437
|
-
const result = addHookToSettings(next, evt.event, cmd, hook.marker
|
|
509
|
+
const result = addHookToSettings(next, evt.event, cmd, hook.marker, {
|
|
510
|
+
matcher: evt.matcher,
|
|
511
|
+
ifRule: evt.if,
|
|
512
|
+
});
|
|
438
513
|
if (result.mutated)
|
|
439
514
|
mutated = true;
|
|
440
515
|
next = result.settings;
|