claude-dev-env 7.2.0 → 7.2.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/.agents/skills/everything-search/SKILL.md +9 -4
- package/.agents/skills/everything-search/scripts/everything_search.py +31 -15
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/constants.py +6 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +68 -1
- package/bin/install.test.mjs +17 -0
- package/hooks/blocking/content_search_to_zoekt_redirector.py +1 -0
- package/hooks/blocking/test_content_search_to_zoekt_redirector.py +46 -0
- package/hooks/hooks.json +2 -2
- package/package.json +1 -1
- package/rules/filesystem-search.md +1 -1
|
@@ -15,7 +15,11 @@ Search files instantly on Windows using the Everything command-line interface (e
|
|
|
15
15
|
|
|
16
16
|
Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds. Narrow the search to what you need.
|
|
17
17
|
|
|
18
|
-
When `es.exe`
|
|
18
|
+
When `es.exe` returns Error 8, the Everything IPC client window is missing. Retry the same search twice with a short pause. Do not report that Everything is not running, not loaded, or that the index is unavailable from that first miss. After those retries fail, report that the Everything IPC client did not answer and that the service state was not probed, then fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents) with the same scope.
|
|
19
|
+
|
|
20
|
+
When `es.exe` is missing, or a later search returns no hits, fall back to `Glob` or `Grep`. A failed search is not proof the index is down. When those fallbacks also fail, ask the user through `AskUserQuestion` with a short analysis and next-step options.
|
|
21
|
+
|
|
22
|
+
Direct `es.exe` calls use the same Error 8 retry. The wrapper in `scripts/everything_search.py` already retries Error 8 twice.
|
|
19
23
|
|
|
20
24
|
## Registry tokens
|
|
21
25
|
|
|
@@ -139,7 +143,8 @@ Run separate searches for each extension type:
|
|
|
139
143
|
|
|
140
144
|
## Best Practices
|
|
141
145
|
|
|
142
|
-
- Everything
|
|
146
|
+
- `es.exe` talks to the Everything tray client IPC window. The Everything service can be running while Error 8 still fires.
|
|
147
|
+
- On Error 8, retry the same search twice before treating the search as failed.
|
|
143
148
|
- Run one extension per search for cleaner results
|
|
144
149
|
- Use `dm:` to filter recent files by the index timestamp
|
|
145
150
|
- Combine with path to narrow scope
|
|
@@ -165,5 +170,5 @@ Everything indexes REAL paths only, not junctions or mapped drives.
|
|
|
165
170
|
|
|
166
171
|
## Files
|
|
167
172
|
|
|
168
|
-
- `scripts/everything_search.py` runs one Everything search without a shell.
|
|
169
|
-
- `scripts/test_everything_search.py` verifies registry expansion and
|
|
173
|
+
- `scripts/everything_search.py` runs one Everything search without a shell, retries Error 8 twice, and reports that the IPC client did not answer and that the service state was not probed when those retries fail.
|
|
174
|
+
- `scripts/test_everything_search.py` verifies registry expansion, command failures, Error 8 retry, and the IPC-client miss message.
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
6
|
import sys
|
|
7
|
+
import time
|
|
7
8
|
from collections.abc import Mapping, Sequence
|
|
8
9
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
9
10
|
from typing import TextIO
|
|
@@ -14,6 +15,10 @@ from everything_search_command_constants.config.constants import (
|
|
|
14
15
|
EXECUTION_ERROR_EXIT_CODE,
|
|
15
16
|
INFORMATIONAL_ARGUMENT,
|
|
16
17
|
INVALID_INPUT_EXIT_CODE,
|
|
18
|
+
IPC_CLIENT_DID_NOT_ANSWER_MESSAGE,
|
|
19
|
+
IPC_WINDOW_NOT_FOUND_EXIT_CODE,
|
|
20
|
+
IPC_WINDOW_RETRY_COUNT,
|
|
21
|
+
IPC_WINDOW_RETRY_DELAY_SECONDS,
|
|
17
22
|
PROJECT_PATHS_FILE_NAME,
|
|
18
23
|
REGISTRY_META_KEY,
|
|
19
24
|
SEARCH_SCOPE_REQUIRED_MESSAGE,
|
|
@@ -115,21 +120,32 @@ def _run_search(
|
|
|
115
120
|
search_stdout: TextIO,
|
|
116
121
|
search_stderr: TextIO,
|
|
117
122
|
) -> int:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
ipc_client_did_not_answer_message = IPC_CLIENT_DID_NOT_ANSWER_MESSAGE
|
|
124
|
+
ipc_window_not_found_exit_code = IPC_WINDOW_NOT_FOUND_EXIT_CODE
|
|
125
|
+
ipc_window_retry_count = IPC_WINDOW_RETRY_COUNT
|
|
126
|
+
ipc_window_retry_delay_seconds = IPC_WINDOW_RETRY_DELAY_SECONDS
|
|
127
|
+
last_search_run = None
|
|
128
|
+
for each_attempt_index in range(ipc_window_retry_count + 1):
|
|
129
|
+
try:
|
|
130
|
+
last_search_run = subprocess.run(
|
|
131
|
+
[executable_path, *all_search_arguments],
|
|
132
|
+
capture_output=True,
|
|
133
|
+
check=False,
|
|
134
|
+
encoding=UTF8_ENCODING,
|
|
135
|
+
shell=False,
|
|
136
|
+
text=True,
|
|
137
|
+
)
|
|
138
|
+
except OSError as error:
|
|
139
|
+
search_stderr.write(f"{error}\n")
|
|
140
|
+
return EXECUTION_ERROR_EXIT_CODE
|
|
141
|
+
if last_search_run.returncode != ipc_window_not_found_exit_code:
|
|
142
|
+
search_stdout.write(last_search_run.stdout)
|
|
143
|
+
search_stderr.write(last_search_run.stderr)
|
|
144
|
+
return last_search_run.returncode
|
|
145
|
+
if each_attempt_index < ipc_window_retry_count:
|
|
146
|
+
time.sleep(ipc_window_retry_delay_seconds)
|
|
147
|
+
search_stderr.write(ipc_client_did_not_answer_message)
|
|
148
|
+
return ipc_window_not_found_exit_code
|
|
133
149
|
|
|
134
150
|
|
|
135
151
|
def _run_registered_search(
|
|
@@ -5,6 +5,12 @@ EXECUTABLE_NAME = "es.exe"
|
|
|
5
5
|
EXECUTION_ERROR_EXIT_CODE = 1
|
|
6
6
|
INFORMATIONAL_ARGUMENT = "-version"
|
|
7
7
|
INVALID_INPUT_EXIT_CODE = 2
|
|
8
|
+
IPC_CLIENT_DID_NOT_ANSWER_MESSAGE = (
|
|
9
|
+
"Everything IPC client did not answer. Everything service state was not probed.\n"
|
|
10
|
+
)
|
|
11
|
+
IPC_WINDOW_NOT_FOUND_EXIT_CODE = 8
|
|
12
|
+
IPC_WINDOW_RETRY_COUNT = 2
|
|
13
|
+
IPC_WINDOW_RETRY_DELAY_SECONDS = 0.5
|
|
8
14
|
PROJECT_PATHS_FILE_NAME = "project-paths.json"
|
|
9
15
|
REGISTRY_META_KEY = "_meta"
|
|
10
16
|
SEARCH_SCOPE_REQUIRED_MESSAGE = "The first search argument must define a path, project, name, extension, date, or size scope.\n"
|
|
@@ -113,12 +113,79 @@ def test_main_passes_through_child_exit_status() -> None:
|
|
|
113
113
|
with (
|
|
114
114
|
patch.object(search, "load_registry", return_value={}),
|
|
115
115
|
patch.object(search.shutil, "which", return_value="es.exe"),
|
|
116
|
-
patch.object(
|
|
116
|
+
patch.object(
|
|
117
|
+
search.subprocess, "run", return_value=completed_process
|
|
118
|
+
) as run_process,
|
|
117
119
|
patch.object(search.sys, "stderr", captured_stderr),
|
|
118
120
|
):
|
|
119
121
|
exit_code = search.main(["config.py"])
|
|
120
122
|
assert exit_code == 7
|
|
121
123
|
assert captured_stderr.getvalue() == "failed\n"
|
|
124
|
+
run_process.assert_called_once()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _ipc_window_error_process() -> search.subprocess.CompletedProcess:
|
|
128
|
+
return search.subprocess.CompletedProcess(
|
|
129
|
+
args=["es.exe", "config.py"],
|
|
130
|
+
returncode=8,
|
|
131
|
+
stdout="",
|
|
132
|
+
stderr=(
|
|
133
|
+
"Error 8: Everything IPC window not found. "
|
|
134
|
+
"Please make sure Everything is running.\n"
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_main_retries_ipc_window_error_then_returns_search_hits() -> None:
|
|
140
|
+
successful_process = search.subprocess.CompletedProcess(
|
|
141
|
+
args=["es.exe", "config.py"],
|
|
142
|
+
returncode=0,
|
|
143
|
+
stdout="found path\n",
|
|
144
|
+
stderr="",
|
|
145
|
+
)
|
|
146
|
+
captured_stdout = io.StringIO()
|
|
147
|
+
captured_stderr = io.StringIO()
|
|
148
|
+
with (
|
|
149
|
+
patch.object(search, "load_registry", return_value={}),
|
|
150
|
+
patch.object(search.shutil, "which", return_value="es.exe"),
|
|
151
|
+
patch.object(
|
|
152
|
+
search.subprocess,
|
|
153
|
+
"run",
|
|
154
|
+
side_effect=[_ipc_window_error_process(), successful_process],
|
|
155
|
+
) as run_process,
|
|
156
|
+
patch.object(search.sys, "stdout", captured_stdout),
|
|
157
|
+
patch.object(search.sys, "stderr", captured_stderr),
|
|
158
|
+
patch("time.sleep"),
|
|
159
|
+
):
|
|
160
|
+
exit_code = search.main(["config.py"])
|
|
161
|
+
assert exit_code == 0
|
|
162
|
+
assert captured_stdout.getvalue() == "found path\n"
|
|
163
|
+
assert "not loaded" not in captured_stderr.getvalue().casefold()
|
|
164
|
+
assert "not running" not in captured_stderr.getvalue().casefold()
|
|
165
|
+
assert run_process.call_count == 2
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_main_reports_ipc_client_miss_after_ipc_window_retries_exhaust() -> None:
|
|
169
|
+
captured_stderr = io.StringIO()
|
|
170
|
+
with (
|
|
171
|
+
patch.object(search, "load_registry", return_value={}),
|
|
172
|
+
patch.object(search.shutil, "which", return_value="es.exe"),
|
|
173
|
+
patch.object(
|
|
174
|
+
search.subprocess,
|
|
175
|
+
"run",
|
|
176
|
+
return_value=_ipc_window_error_process(),
|
|
177
|
+
) as run_process,
|
|
178
|
+
patch.object(search.sys, "stderr", captured_stderr),
|
|
179
|
+
patch("time.sleep"),
|
|
180
|
+
):
|
|
181
|
+
exit_code = search.main(["config.py"])
|
|
182
|
+
assert exit_code == 8
|
|
183
|
+
stderr_text = captured_stderr.getvalue()
|
|
184
|
+
assert "Everything IPC client did not answer" in stderr_text
|
|
185
|
+
assert "service state was not probed" in stderr_text.casefold()
|
|
186
|
+
assert "Please make sure Everything is running" not in stderr_text
|
|
187
|
+
assert "not loaded" not in stderr_text.casefold()
|
|
188
|
+
assert run_process.call_count == 3
|
|
122
189
|
|
|
123
190
|
|
|
124
191
|
def test_main_reports_missing_executable() -> None:
|
package/bin/install.test.mjs
CHANGED
|
@@ -1353,6 +1353,23 @@ test('mergeHooksIntoSettings is idempotent when run twice against an already-upd
|
|
|
1353
1353
|
});
|
|
1354
1354
|
|
|
1355
1355
|
|
|
1356
|
+
test('shipped SessionEnd hook timeouts stay within the Codex runtime limit', () => {
|
|
1357
|
+
const shippedHooksConfig = JSON.parse(
|
|
1358
|
+
readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8')
|
|
1359
|
+
);
|
|
1360
|
+
const allSessionEndHooks = (shippedHooksConfig.hooks.SessionEnd || [])
|
|
1361
|
+
.flatMap(eachMatcherGroup => eachMatcherGroup.hooks);
|
|
1362
|
+
|
|
1363
|
+
assert.ok(allSessionEndHooks.length > 0, 'shipped hooks.json must register SessionEnd hooks');
|
|
1364
|
+
for (const eachSessionEndHook of allSessionEndHooks) {
|
|
1365
|
+
assert.ok(
|
|
1366
|
+
eachSessionEndHook.timeout <= 3,
|
|
1367
|
+
`${eachSessionEndHook.command} exceeds the Codex SessionEnd runtime limit`,
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
|
|
1356
1373
|
test('shipped hooks.json keeps only nonblocking policy handlers and lifecycle hooks', () => {
|
|
1357
1374
|
const shippedHooksConfig = JSON.parse(
|
|
1358
1375
|
readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Retired search hook entrypoint kept inert for stale command references."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Regression tests for the inert retired search hook entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_BLOCKING_DIRECTORY = Path(__file__).resolve().parent
|
|
12
|
+
_ENTRYPOINT_PATH = _BLOCKING_DIRECTORY / "content_search_to_zoekt_redirector.py"
|
|
13
|
+
_HOOKS_MANIFEST_PATH = _BLOCKING_DIRECTORY.parent / "hooks.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_retired_entrypoint_allows_valid_search_events_silently() -> None:
|
|
17
|
+
all_search_events = (
|
|
18
|
+
{"tool_name": "Bash", "tool_input": {"command": "rg --files packages"}},
|
|
19
|
+
{
|
|
20
|
+
"tool_name": "Grep",
|
|
21
|
+
"tool_input": {"pattern": "zoekt", "path": "packages"},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"tool_name": "Search",
|
|
25
|
+
"tool_input": {"pattern": "zoekt", "path": "packages"},
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for each_search_event in all_search_events:
|
|
30
|
+
completed_process = subprocess.run(
|
|
31
|
+
[sys.executable, str(_ENTRYPOINT_PATH)],
|
|
32
|
+
check=False,
|
|
33
|
+
input=json.dumps(each_search_event),
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
encoding="utf-8",
|
|
37
|
+
)
|
|
38
|
+
assert completed_process.returncode == 0
|
|
39
|
+
assert completed_process.stdout == ""
|
|
40
|
+
assert completed_process.stderr == ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_shipped_hook_manifest_omits_retired_entrypoint() -> None:
|
|
44
|
+
hooks_manifest_text = _HOOKS_MANIFEST_PATH.read_text(encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
assert _ENTRYPOINT_PATH.name not in hooks_manifest_text
|
package/hooks/hooks.json
CHANGED
|
@@ -84,12 +84,12 @@
|
|
|
84
84
|
{
|
|
85
85
|
"type": "command",
|
|
86
86
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/session_end_cleanup.py",
|
|
87
|
-
"timeout":
|
|
87
|
+
"timeout": 3
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"type": "command",
|
|
91
91
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/session_edit_tracker_cleanup.py",
|
|
92
|
-
"timeout":
|
|
92
|
+
"timeout": 3
|
|
93
93
|
}
|
|
94
94
|
]
|
|
95
95
|
}
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ Three tools are equally sanctioned; pick by what you know:
|
|
|
19
19
|
| A name or path pattern | The harness `Glob` tool |
|
|
20
20
|
| Text inside files | The harness `Grep` tool |
|
|
21
21
|
|
|
22
|
-
When `es.exe`
|
|
22
|
+
When `es.exe` returns Error 8, retry the same search twice before any fallback. Error 8 is a missing IPC client window, not proof the index is down. After those retries fail, report that the Everything IPC client did not answer and that the service state was not probed, then fall back to `Glob` or `Grep` with the same scope. When `es.exe` is missing, or a later search returns no hits, fall back to `Glob` or `Grep`. Ask the user only after all three tools fail.
|
|
23
23
|
|
|
24
24
|
`skills/everything-search/SKILL.md` holds the full `es.exe` operator reference: `ext:`, `dm:`, `size:`, wildcards, OR/AND/NOT, output flags, and the junction and drive-mapping note.
|
|
25
25
|
|