claude-dev-env 4.0.0 → 4.2.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/.agents/skills/everything-search/SKILL.md +18 -3
- package/.agents/skills/everything-search/scripts/everything_search.py +204 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/constants.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +172 -0
- package/.agents/skills/syncing-submodules/SKILL.md +101 -0
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
- package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
- package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/ever-shipped-skills.test.mjs +30 -0
- package/bin/git_hooks_installer.mjs +0 -1
- package/bin/git_hooks_installer.test.mjs +16 -1
- package/bin/install.mjs +5 -2
- package/bin/install.prune.test.mjs +95 -24
- package/bin/install.test.mjs +7 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +5 -8
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +7 -13
- package/package.json +1 -1
- package/rules/filesystem-search.md +5 -5
- package/hooks/blocking/es_exe_path_rewriter.py +0 -159
- package/hooks/blocking/test_es_exe_path_rewriter.py +0 -447
- package/hooks/git-hooks/post_commit.py +0 -241
- package/hooks/git-hooks/test_post_commit.py +0 -298
- package/hooks/hooks_constants/path_rewriter_constants.py +0 -13
- package/hooks/hooks_constants/test_path_rewriter_constants.py +0 -57
|
@@ -35,7 +35,9 @@ BASH_TOOL_NAME = "Bash"
|
|
|
35
35
|
POWERSHELL_TOOL_NAME = "PowerShell"
|
|
36
36
|
|
|
37
37
|
ALL_BASH_ONLY_TOOL_NAMES: frozenset[str] = frozenset({BASH_TOOL_NAME})
|
|
38
|
-
ALL_BASH_AND_POWERSHELL_TOOL_NAMES: frozenset[str] = frozenset(
|
|
38
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES: frozenset[str] = frozenset(
|
|
39
|
+
{BASH_TOOL_NAME, POWERSHELL_TOOL_NAME}
|
|
40
|
+
)
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
@dataclass(frozen=True)
|
|
@@ -53,7 +55,6 @@ class BashHostedHookEntry:
|
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
56
|
-
BashHostedHookEntry("blocking/es_exe_path_rewriter.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
57
58
|
BashHostedHookEntry("blocking/destructive_command_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
58
59
|
BashHostedHookEntry("blocking/gh_body_arg_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
59
60
|
BashHostedHookEntry("blocking/shell_substitution_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
@@ -62,14 +63,10 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
|
62
63
|
"blocking/cursor_cli_python_misfire_blocker.py",
|
|
63
64
|
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
64
65
|
),
|
|
65
|
-
BashHostedHookEntry(
|
|
66
|
-
"blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
67
|
-
),
|
|
66
|
+
BashHostedHookEntry("blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
68
67
|
BashHostedHookEntry("blocking/nas_ssh_binary_enforcer.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
69
68
|
BashHostedHookEntry("blocking/volatile_path_in_post_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
70
|
-
BashHostedHookEntry(
|
|
71
|
-
"blocking/pii_prevention_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
72
|
-
),
|
|
69
|
+
BashHostedHookEntry("blocking/pii_prevention_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
73
70
|
BashHostedHookEntry("blocking/conventional_pr_title_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
74
71
|
BashHostedHookEntry("blocking/block_main_commit.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
75
72
|
BashHostedHookEntry("blocking/session_edit_stage_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
@@ -7,22 +7,18 @@ registration order the standalone chain used.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import sys
|
|
11
10
|
from pathlib import Path
|
|
11
|
+
from runpy import run_path
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
sys.path.insert(0, _HOOKS_DIR)
|
|
16
|
-
|
|
17
|
-
from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
|
|
18
|
-
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
19
|
-
ALL_BASH_HOSTED_HOOK_ENTRIES,
|
|
20
|
-
BASH_TOOL_NAME,
|
|
21
|
-
POWERSHELL_TOOL_NAME,
|
|
13
|
+
ALL_CONSTANT_BINDINGS = run_path(
|
|
14
|
+
str(Path(__file__).with_name("bash_pre_tool_use_dispatcher_constants.py"))
|
|
22
15
|
)
|
|
16
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES = ALL_CONSTANT_BINDINGS["ALL_BASH_AND_POWERSHELL_TOOL_NAMES"]
|
|
17
|
+
ALL_BASH_HOSTED_HOOK_ENTRIES = ALL_CONSTANT_BINDINGS["ALL_BASH_HOSTED_HOOK_ENTRIES"]
|
|
18
|
+
BASH_TOOL_NAME = ALL_CONSTANT_BINDINGS["BASH_TOOL_NAME"]
|
|
19
|
+
POWERSHELL_TOOL_NAME = ALL_CONSTANT_BINDINGS["POWERSHELL_TOOL_NAME"]
|
|
23
20
|
|
|
24
21
|
_EXPECTED_BASH_ORDER = (
|
|
25
|
-
"blocking/es_exe_path_rewriter.py",
|
|
26
22
|
"blocking/destructive_command_blocker.py",
|
|
27
23
|
"blocking/gh_body_arg_blocker.py",
|
|
28
24
|
"blocking/shell_substitution_blocker.py",
|
|
@@ -80,8 +76,6 @@ def test_powershell_hooks_carry_the_shared_tool_set() -> None:
|
|
|
80
76
|
assert each_entry.applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
81
77
|
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
79
|
def test_every_roster_path_points_at_an_existing_hook() -> None:
|
|
86
80
|
"""Each roster path names a hook script present on disk, catching a typo."""
|
|
87
81
|
hooks_root = Path(__file__).resolve().parent.parent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Filesystem Search
|
|
2
2
|
|
|
3
|
-
**When this applies:** Any search for files by name, path, extension, size, or date
|
|
3
|
+
**When this applies:** Any search for files by name, path, extension, size, or date. That includes `es.exe`, a shell `find`, a recursive `Get-ChildItem` / `gci` / `dir` / `ls -R`, or the harness Grep and Glob tools.
|
|
4
4
|
|
|
5
5
|
## The scope invariant
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Three tools are equally sanctioned; pick by what you know:
|
|
|
14
14
|
|
|
15
15
|
| You know | Use |
|
|
16
16
|
|---|---|
|
|
17
|
-
| The exact path | `Read
|
|
17
|
+
| The exact path | `Read`. Do not search. |
|
|
18
18
|
| A name, extension, or date, on Windows | `es.exe` with a path scope |
|
|
19
19
|
| A name or path pattern | The harness `Glob` tool |
|
|
20
20
|
| Text inside files | The harness `Grep` tool |
|
|
@@ -39,13 +39,13 @@ When `es.exe` fails or returns nothing, fall back to `Glob` or `Grep` without pa
|
|
|
39
39
|
| Git Bash drive root | `find /c -name '*.py'` |
|
|
40
40
|
| Windows drive root | `find C:\ -name foo` / `Get-ChildItem C:\ -Recurse` |
|
|
41
41
|
| Bare home | `find ~ -name README.md` / `find $HOME -type f` |
|
|
42
|
-
| Network share root | `find //server/share -name x
|
|
42
|
+
| Network share root | `find //server/share -name x`. A path under the share (`//server/share/project/src`) is allowed |
|
|
43
43
|
|
|
44
44
|
## Shell batching
|
|
45
45
|
|
|
46
46
|
Issue one shell search at a time when the walk is large. Parallel full-tree searches contend for the shell and can lock the host. Harness `Grep` and `Glob` calls carry no such cost and run in parallel freely.
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## Search controls
|
|
49
49
|
|
|
50
50
|
- `unscoped_search_blocker` (PreToolUse on Bash and PowerShell, hosted by `bash_pre_tool_use_dispatcher`) denies a walk from an unscoped root and returns the scoped alternative.
|
|
51
|
-
-
|
|
51
|
+
- For Everything searches that use a project name, run `python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>`. An exact project name becomes its path from `~/.claude/project-paths.json`. The command then starts `es.exe` without a shell. `scripts/setup_project_paths.py` writes the registry.
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: rewrite es.exe commands by substituting registry tokens with absolute paths.
|
|
3
|
-
|
|
4
|
-
Reads tool_input.command from stdin JSON. When the command invokes the Everything
|
|
5
|
-
command-line binary, substitutes {project-name} placeholder tokens and bare registry-key
|
|
6
|
-
tokens with their quoted absolute paths from ~/.claude/project-paths.json before
|
|
7
|
-
the Bash call runs. Never blocks or denies — on any error exits 0 with empty output.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import logging
|
|
14
|
-
import re
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
17
|
-
|
|
18
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
19
|
-
if _hooks_dir not in sys.path:
|
|
20
|
-
sys.path.insert(0, _hooks_dir)
|
|
21
|
-
|
|
22
|
-
from hooks_constants.dynamic_stderr_handler import DynamicStderrHandler # noqa: E402
|
|
23
|
-
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
|
|
24
|
-
from hooks_constants.path_rewriter_constants import ( # noqa: E402
|
|
25
|
-
BASH_TOOL_NAME,
|
|
26
|
-
HOOK_EVENT_NAME,
|
|
27
|
-
PERMISSION_ALLOW,
|
|
28
|
-
PLACEHOLDER_TOKEN_PATTERN,
|
|
29
|
-
)
|
|
30
|
-
from hooks_constants.project_paths_reader import load_registry # noqa: E402
|
|
31
|
-
|
|
32
|
-
_ES_EXE_TRIGGER_PATTERN = re.compile(
|
|
33
|
-
r"(?i)(?<![\w.])(?:Everything[/\\])?es\.exe(?![\w.])",
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
_logger = logging.getLogger("es_exe_path_rewriter")
|
|
38
|
-
if not _logger.handlers:
|
|
39
|
-
_stderr_handler = DynamicStderrHandler()
|
|
40
|
-
_stderr_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
|
|
41
|
-
_logger.addHandler(_stderr_handler)
|
|
42
|
-
_logger.setLevel(logging.INFO)
|
|
43
|
-
_logger.propagate = False
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def command_invokes_es_exe(command: str) -> bool:
|
|
47
|
-
"""Return True when the command string contains an es.exe invocation."""
|
|
48
|
-
return bool(_ES_EXE_TRIGGER_PATTERN.search(command))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _token_is_absolute_path(token: str) -> bool:
|
|
52
|
-
stripped = token.strip("\"'")
|
|
53
|
-
try:
|
|
54
|
-
return (
|
|
55
|
-
PureWindowsPath(stripped).is_absolute()
|
|
56
|
-
or PurePosixPath(stripped).is_absolute()
|
|
57
|
-
)
|
|
58
|
-
except (ValueError, TypeError):
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _quote_path(absolute_path: str) -> str:
|
|
63
|
-
return f'"{absolute_path}"'
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _rewrite_placeholder_tokens(command_suffix: str, registry: dict[str, str]) -> str:
|
|
67
|
-
def replace_placeholder(match: re.Match) -> str:
|
|
68
|
-
inner_name = match.group(1)
|
|
69
|
-
if inner_name not in registry:
|
|
70
|
-
return match.group(0)
|
|
71
|
-
return _quote_path(registry[inner_name])
|
|
72
|
-
|
|
73
|
-
return PLACEHOLDER_TOKEN_PATTERN.sub(replace_placeholder, command_suffix)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _split_on_es_exe(command: str) -> tuple[str, str]:
|
|
77
|
-
match = _ES_EXE_TRIGGER_PATTERN.search(command)
|
|
78
|
-
if not match:
|
|
79
|
-
return command, ""
|
|
80
|
-
return command[: match.end()], command[match.end() :]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _strip_matching_outer_quotes(token: str) -> tuple[str, bool]:
|
|
84
|
-
"""Return (inner_text, was_quoted) after removing matched outer quotes."""
|
|
85
|
-
if len(token) > 1 and token[0] in ('"', "'") and token[-1] == token[0]:
|
|
86
|
-
return token[1:-1], True
|
|
87
|
-
return token, False
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _rewrite_bare_tokens(command_suffix: str, registry: dict[str, str]) -> str:
|
|
91
|
-
all_raw_parts = re.split(r"(\s+)", command_suffix)
|
|
92
|
-
all_rewritten_parts: list[str] = []
|
|
93
|
-
for each_raw_part in all_raw_parts:
|
|
94
|
-
if not each_raw_part or each_raw_part.isspace():
|
|
95
|
-
all_rewritten_parts.append(each_raw_part)
|
|
96
|
-
continue
|
|
97
|
-
unquoted_text, _was_quoted = _strip_matching_outer_quotes(each_raw_part)
|
|
98
|
-
if unquoted_text in registry and not _token_is_absolute_path(unquoted_text):
|
|
99
|
-
all_rewritten_parts.append(_quote_path(registry[unquoted_text]))
|
|
100
|
-
else:
|
|
101
|
-
all_rewritten_parts.append(each_raw_part)
|
|
102
|
-
return "".join(all_rewritten_parts)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def rewrite_command(command: str, registry: dict[str, str]) -> str:
|
|
106
|
-
"""Apply registry substitutions to any es.exe argument tokens.
|
|
107
|
-
|
|
108
|
-
Applies placeholder form {name} first, then bare-token form.
|
|
109
|
-
Absolute-path arguments and unknown tokens are left untouched.
|
|
110
|
-
Returns the original command when no substitution applies.
|
|
111
|
-
"""
|
|
112
|
-
if not registry:
|
|
113
|
-
return command
|
|
114
|
-
prefix, suffix = _split_on_es_exe(command)
|
|
115
|
-
rewritten_suffix = _rewrite_placeholder_tokens(suffix, registry)
|
|
116
|
-
rewritten_suffix = _rewrite_bare_tokens(rewritten_suffix, registry)
|
|
117
|
-
return prefix + rewritten_suffix
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _build_allow_response(rewritten_command: str, original_tool_input: dict) -> dict:
|
|
121
|
-
updated_input = {**original_tool_input, "command": rewritten_command}
|
|
122
|
-
return {
|
|
123
|
-
"hookSpecificOutput": {
|
|
124
|
-
"hookEventName": HOOK_EVENT_NAME,
|
|
125
|
-
"permissionDecision": PERMISSION_ALLOW,
|
|
126
|
-
"updatedInput": updated_input,
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def main() -> None:
|
|
132
|
-
try:
|
|
133
|
-
hook_input = read_hook_input_dictionary_from_stdin()
|
|
134
|
-
if hook_input is None:
|
|
135
|
-
sys.exit(0)
|
|
136
|
-
raw_tool_name = hook_input.get("tool_name", "")
|
|
137
|
-
raw_tool_input = hook_input.get("tool_input", {})
|
|
138
|
-
tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
|
|
139
|
-
tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
|
|
140
|
-
if tool_name != BASH_TOOL_NAME:
|
|
141
|
-
sys.exit(0)
|
|
142
|
-
raw_command = tool_input.get("command", "")
|
|
143
|
-
command = raw_command if isinstance(raw_command, str) else ""
|
|
144
|
-
if not command_invokes_es_exe(command):
|
|
145
|
-
sys.exit(0)
|
|
146
|
-
known_registry = load_registry()
|
|
147
|
-
if not known_registry:
|
|
148
|
-
sys.exit(0)
|
|
149
|
-
rewritten_command = rewrite_command(command, known_registry)
|
|
150
|
-
if rewritten_command == command:
|
|
151
|
-
sys.exit(0)
|
|
152
|
-
print(json.dumps(_build_allow_response(rewritten_command, tool_input)))
|
|
153
|
-
except Exception as e:
|
|
154
|
-
_logger.error("%s", e)
|
|
155
|
-
sys.exit(0)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if __name__ == "__main__":
|
|
159
|
-
main()
|