stashai 0.1.332__tar.gz → 0.1.334__tar.gz

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.
Files changed (139) hide show
  1. {stashai-0.1.332/stashai.egg-info → stashai-0.1.334}/PKG-INFO +1 -1
  2. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_app_vfs.py +11 -10
  3. {stashai-0.1.332 → stashai-0.1.334}/pyproject.toml +1 -1
  4. {stashai-0.1.332 → stashai-0.1.334/stashai.egg-info}/PKG-INFO +1 -1
  5. {stashai-0.1.332 → stashai-0.1.334}/stashvfs/__init__.py +2 -1
  6. {stashai-0.1.332 → stashai-0.1.334}/stashvfs/client.py +10 -0
  7. {stashai-0.1.332 → stashai-0.1.334}/stashvfs/model.py +5 -5
  8. {stashai-0.1.332 → stashai-0.1.334}/stashvfs/shell.py +52 -25
  9. {stashai-0.1.332 → stashai-0.1.334}/LICENSE +0 -0
  10. {stashai-0.1.332 → stashai-0.1.334}/README.md +0 -0
  11. {stashai-0.1.332 → stashai-0.1.334}/cli/__init__.py +0 -0
  12. {stashai-0.1.332 → stashai-0.1.334}/cli/client.py +0 -0
  13. {stashai-0.1.332 → stashai-0.1.334}/cli/config.py +0 -0
  14. {stashai-0.1.332 → stashai-0.1.334}/cli/formatting.py +0 -0
  15. {stashai-0.1.332 → stashai-0.1.334}/cli/import_history.py +0 -0
  16. {stashai-0.1.332 → stashai-0.1.334}/cli/main.py +0 -0
  17. {stashai-0.1.332 → stashai-0.1.334}/cli/mcp_server.py +0 -0
  18. {stashai-0.1.332 → stashai-0.1.334}/cli/telemetry.py +0 -0
  19. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/__init__.py +0 -0
  20. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_agent_cli.py +0 -0
  21. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_agent_detection.py +0 -0
  22. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_client_retry.py +0 -0
  23. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_history_pagination.py +0 -0
  24. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_hook_cli.py +0 -0
  25. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_import_history.py +0 -0
  26. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_install_codex.py +0 -0
  27. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_install_gemini.py +0 -0
  28. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_install_hermes.py +0 -0
  29. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_install_hooks.py +0 -0
  30. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_install_openclaw.py +0 -0
  31. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_ls_cmd.py +0 -0
  32. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_marketplace_autoupdate.py +0 -0
  33. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_memory_recompute_poll.py +0 -0
  34. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_mount_vfs.py +0 -0
  35. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_root_help.py +0 -0
  36. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_scope_config.py +0 -0
  37. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_setup_complete_splash.py +0 -0
  38. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_share_and_upload_helpers.py +0 -0
  39. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_sharing_cli.py +0 -0
  40. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_skill_client_permissions.py +0 -0
  41. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_skills_install.py +0 -0
  42. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_skills_installed.py +0 -0
  43. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_skills_sync.py +0 -0
  44. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_sources_cli.py +0 -0
  45. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_streaming_toggle.py +0 -0
  46. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_tools_cli.py +0 -0
  47. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_update_file_and_folder.py +0 -0
  48. {stashai-0.1.332 → stashai-0.1.334}/cli/tests/test_upload_status_helpers.py +0 -0
  49. {stashai-0.1.332 → stashai-0.1.334}/setup.cfg +0 -0
  50. {stashai-0.1.332 → stashai-0.1.334}/stashai/__init__.py +0 -0
  51. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/__init__.py +0 -0
  52. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/_do_session_upload.py +0 -0
  53. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/_do_upload.py +0 -0
  54. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/_session_watcher.py +0 -0
  55. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/agent_config.py +0 -0
  56. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/__init__.py +0 -0
  57. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/claude/scripts/adapt.py +0 -0
  58. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/claude/scripts/config.py +0 -0
  59. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/claude/scripts/on_prompt.py +0 -0
  60. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/claude/scripts/on_session_end.py +0 -0
  61. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/claude/scripts/on_session_start.py +0 -0
  62. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/claude/scripts/on_stop.py +0 -0
  63. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/claude/scripts/on_tool_use.py +0 -0
  64. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/AGENTS.md +0 -0
  65. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/README.md +0 -0
  66. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/config.toml.snippet +0 -0
  67. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/hooks.json +0 -0
  68. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/scripts/adapt.py +0 -0
  69. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/scripts/config.py +0 -0
  70. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/scripts/on_prompt.py +0 -0
  71. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/scripts/on_session_start.py +0 -0
  72. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/scripts/on_stop.py +0 -0
  73. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/codex/scripts/on_tool_use.py +0 -0
  74. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/README.md +0 -0
  75. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/hooks.json +0 -0
  76. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/scripts/adapt.py +0 -0
  77. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/scripts/config.py +0 -0
  78. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/scripts/on_agent_response.py +0 -0
  79. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/scripts/on_prompt.py +0 -0
  80. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/scripts/on_session_end.py +0 -0
  81. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/scripts/on_session_start.py +0 -0
  82. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/scripts/on_tool_use.py +0 -0
  83. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/cursor/stash.mdc +0 -0
  84. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/GEMINI.md +0 -0
  85. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/README.md +0 -0
  86. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/scripts/adapt.py +0 -0
  87. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/scripts/config.py +0 -0
  88. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/scripts/on_prompt.py +0 -0
  89. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/scripts/on_session_end.py +0 -0
  90. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/scripts/on_session_start.py +0 -0
  91. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/scripts/on_stop.py +0 -0
  92. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/scripts/on_tool_use.py +0 -0
  93. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/gemini/settings.snippet.json +0 -0
  94. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/HERMES.md +0 -0
  95. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/README.md +0 -0
  96. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/config.snippet.yaml +0 -0
  97. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/scripts/adapt.py +0 -0
  98. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/scripts/config.py +0 -0
  99. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/scripts/on_prompt.py +0 -0
  100. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/scripts/on_session_end.py +0 -0
  101. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/scripts/on_session_start.py +0 -0
  102. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/scripts/on_stop.py +0 -0
  103. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/hermes/scripts/on_tool_use.py +0 -0
  104. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/README.md +0 -0
  105. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/dist/index.js +0 -0
  106. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/index.ts +0 -0
  107. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/openclaw.plugin.json +0 -0
  108. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/package.json +0 -0
  109. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/scripts/adapt.py +0 -0
  110. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/scripts/config.py +0 -0
  111. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/scripts/on_prompt.py +0 -0
  112. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/scripts/on_session_end.py +0 -0
  113. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/scripts/on_session_start.py +0 -0
  114. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/openclaw/scripts/on_stop.py +0 -0
  115. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/AGENTS.md +0 -0
  116. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/README.md +0 -0
  117. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/package.json +0 -0
  118. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/plugin.ts +0 -0
  119. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/scripts/adapt.py +0 -0
  120. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/scripts/config.py +0 -0
  121. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/scripts/on_prompt.py +0 -0
  122. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/scripts/on_session_end.py +0 -0
  123. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/scripts/on_session_start.py +0 -0
  124. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/assets/opencode/scripts/on_tool_use.py +0 -0
  125. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/doctor.py +0 -0
  126. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/event.py +0 -0
  127. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/hooks.py +0 -0
  128. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/scope.py +0 -0
  129. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/session_upload.py +0 -0
  130. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/stash_client.py +0 -0
  131. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/state.py +0 -0
  132. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/summarize.py +0 -0
  133. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/transcript_upload.py +0 -0
  134. {stashai-0.1.332 → stashai-0.1.334}/stashai/plugin/upload_status.py +0 -0
  135. {stashai-0.1.332 → stashai-0.1.334}/stashai.egg-info/SOURCES.txt +0 -0
  136. {stashai-0.1.332 → stashai-0.1.334}/stashai.egg-info/dependency_links.txt +0 -0
  137. {stashai-0.1.332 → stashai-0.1.334}/stashai.egg-info/entry_points.txt +0 -0
  138. {stashai-0.1.332 → stashai-0.1.334}/stashai.egg-info/requires.txt +0 -0
  139. {stashai-0.1.332 → stashai-0.1.334}/stashai.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stashai
3
- Version: 0.1.332
3
+ Version: 0.1.334
4
4
  Summary: CLI for Stash — shared memory for AI coding agents
5
5
  Author: Fergana Labs
6
6
  License: MIT
@@ -190,21 +190,22 @@ def test_app_vfs_grep_no_match_stops_and_chain():
190
190
  assert result.stderr == ""
191
191
 
192
192
 
193
- def test_app_vfs_grep_rejects_bre_escapes_loudly():
194
- """Patterns compile as extended regex, where BRE's `\\|` means a literal pipe
195
- a default-grep caller writing `foo\\|bar` would silently match nothing (this
196
- burned Heavi's agent in prod). The shell must refuse with a correction, never
197
- return a clean no-match exit 1."""
193
+ def test_app_vfs_grep_accepts_bre_operator_escapes():
194
+ """Patterns compile as extended regex, but default-grep callers write BRE
195
+ `foo\\|bar` (this burned Heavi's agent in prod, twice: first as a silent
196
+ no-match, then as a hard refusal that cost the agent its search). Both
197
+ dialects intend an operator there, so the escape must just work; `-F` stays
198
+ the literal-match path."""
198
199
  shell, _client = _shell()
199
200
 
200
- result = shell.run("grep -ri 'hello\\|missing' /")
201
+ assert shell.run("grep -ri 'hello\\|missing' /").exit_code == 0
201
202
 
202
- assert result.exit_code == 2
203
- assert "extended" in result.stderr
204
-
205
- # The same intent written as extended regex works.
203
+ # The same intent written as extended regex still works.
206
204
  assert shell.run("grep -ri 'hello|missing' /").exit_code == 0
207
205
 
206
+ # A literal backslash escape that is not a BRE operator is left alone.
207
+ assert shell.run("grep -ri 'hello\\d' /").exit_code == 1
208
+
208
209
 
209
210
  def test_app_vfs_grep_accepts_extended_and_fixed_string_flags():
210
211
  shell, _client = _shell()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stashai"
3
- version = "0.1.332"
3
+ version = "0.1.334"
4
4
  description = "CLI for Stash — shared memory for AI coding agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stashai
3
- Version: 0.1.332
3
+ Version: 0.1.334
4
4
  Summary: CLI for Stash — shared memory for AI coding agents
5
5
  Author: Fergana Labs
6
6
  License: MIT
@@ -2,7 +2,7 @@
2
2
  contract that backs them. Shared by the `stash vfs` CLI command and the
3
3
  `/api/v1/me/vfs` endpoint."""
4
4
 
5
- from .client import MachineVfsClient, VfsClient, VfsClientError
5
+ from .client import MachineVfsClient, VfsClient, VfsClientError, VfsScanBudget
6
6
  from .model import MountError, StashVfsModel
7
7
  from .shell import SkillAppVfsShell, VfsCommandResult
8
8
 
@@ -14,4 +14,5 @@ __all__ = [
14
14
  "VfsClient",
15
15
  "VfsClientError",
16
16
  "VfsCommandResult",
17
+ "VfsScanBudget",
17
18
  ]
@@ -25,6 +25,16 @@ class VfsClientError(Exception):
25
25
  super().__init__(str(detail))
26
26
 
27
27
 
28
+ class VfsScanBudget(Exception):
29
+ """A document read was refused because the command's scan budget ran out.
30
+
31
+ Raised by clients that meter reads (the server-side VFS) for reads issued
32
+ inside `scan_calls`. The shell stops the grep sweep at this point and
33
+ reports the results so far with a loud truncation warning — an org-wide
34
+ sweep degrades to a partial answer instead of an aborted command.
35
+ """
36
+
37
+
28
38
  class VfsClient(Protocol):
29
39
  """Everything `StashVfsModel` reads. Listing calls run during `refresh()`;
30
40
  the rest are lazy loaders fired when a file's bytes are first read."""
@@ -15,7 +15,7 @@ from concurrent.futures import ThreadPoolExecutor
15
15
  from dataclasses import dataclass, field
16
16
  from datetime import datetime
17
17
 
18
- from .client import MachineVfsClient, VfsClient, VfsClientError
18
+ from .client import MachineVfsClient, VfsClient, VfsClientError, VfsScanBudget
19
19
 
20
20
  BytesLoader = Callable[[], bytes]
21
21
 
@@ -57,7 +57,7 @@ class VfsNode:
57
57
  # Set when `prefetch` already tried this loader and it failed. `read_file`
58
58
  # re-raises it rather than fetching again — a second fetch would hit the
59
59
  # provider twice and, server-side, spend the document budget twice.
60
- error: VfsClientError | None = None
60
+ error: VfsClientError | VfsScanBudget | None = None
61
61
 
62
62
  @property
63
63
  def is_dir(self) -> bool:
@@ -180,16 +180,16 @@ class StashVfsModel:
180
180
  if len(pending) < 2:
181
181
  return
182
182
 
183
- def load(node: VfsNode) -> tuple[VfsNode, bytes | VfsClientError]:
183
+ def load(node: VfsNode) -> tuple[VfsNode, bytes | VfsClientError | VfsScanBudget]:
184
184
  try:
185
185
  return node, node.loader()
186
- except VfsClientError as e:
186
+ except (VfsClientError, VfsScanBudget) as e:
187
187
  return node, e
188
188
 
189
189
  workers = min(PREFETCH_CONCURRENCY, len(pending))
190
190
  with ThreadPoolExecutor(max_workers=workers) as pool:
191
191
  for node, result in pool.map(load, pending):
192
- if isinstance(result, VfsClientError):
192
+ if isinstance(result, (VfsClientError, VfsScanBudget)):
193
193
  node.error = result
194
194
  continue
195
195
  node.content = result
@@ -10,7 +10,7 @@ import time
10
10
  from dataclasses import dataclass
11
11
  from datetime import datetime
12
12
 
13
- from .client import VfsClientError
13
+ from .client import VfsClientError, VfsScanBudget
14
14
  from .model import MountError, StashVfsModel
15
15
 
16
16
 
@@ -435,9 +435,8 @@ class SkillAppVfsShell:
435
435
  if fixed_strings:
436
436
  regex = re.compile(re.escape(pattern), flags)
437
437
  else:
438
- _reject_bre_escapes(pattern)
439
438
  try:
440
- regex = re.compile(pattern, flags)
439
+ regex = re.compile(_translate_bre_escapes(pattern), flags)
441
440
  except re.error as e:
442
441
  raise VfsShellError(str(e)) from e
443
442
 
@@ -459,27 +458,44 @@ class SkillAppVfsShell:
459
458
  paths = ["."]
460
459
  matches = []
461
460
  roots = []
461
+ # Resolve every root before reading anything (listings are unbudgeted),
462
+ # so a budget stop can say how much of the sweep went unsearched.
463
+ sweep: list[list[str]] = []
464
+ total_files = 0
465
+ for raw_path in paths:
466
+ path = self._resolve_path(raw_path)
467
+ roots.append(path)
468
+ node = self.model._get_node(path)
469
+ file_paths = [path] if node.is_file else self._file_paths(path) if recursive else []
470
+ if node.is_dir and not recursive:
471
+ raise VfsShellError(f"{raw_path}: is a directory")
472
+ sweep.append(file_paths)
473
+ total_files += len(file_paths)
462
474
  docs_scanned = 0
475
+ budget_hit = False
463
476
  # The per-document reads below are the mechanics of one search, not
464
477
  # documents the user asked to see — scan_calls tags them so analytics
465
478
  # skip them, and record_search writes the single search event that
466
479
  # stands in for the whole sweep.
467
480
  with self.model.client.scan_calls():
468
- for raw_path in paths:
469
- path = self._resolve_path(raw_path)
470
- roots.append(path)
471
- node = self.model._get_node(path)
472
- file_paths = [path] if node.is_file else self._file_paths(path) if recursive else []
473
- if node.is_dir and not recursive:
474
- raise VfsShellError(f"{raw_path}: is a directory")
481
+ for file_paths in sweep:
482
+ if budget_hit:
483
+ break
475
484
  self.model.prefetch(file_paths)
476
485
  for file_path in file_paths:
477
- docs_scanned += 1
478
486
  try:
479
487
  text = self._read_text(file_path)
488
+ except VfsScanBudget:
489
+ # Out of reads mid-sweep: report what was found rather
490
+ # than aborting, but never silently — the warning below
491
+ # marks the results partial.
492
+ budget_hit = True
493
+ break
480
494
  except VfsClientError as e:
495
+ docs_scanned += 1
481
496
  self._warn(f"{name}: {file_path}: {e.detail}")
482
497
  continue
498
+ docs_scanned += 1
483
499
  matches.append(
484
500
  _grep_text(
485
501
  regex,
@@ -492,6 +508,12 @@ class SkillAppVfsShell:
492
508
  )
493
509
  )
494
510
  self.model.client.record_search(pattern, roots, docs_scanned)
511
+ if budget_hit:
512
+ self._warn(
513
+ f"{name}: stopped after reading {docs_scanned} of {total_files} files "
514
+ "(server scan budget); matches shown are partial — narrow the paths to "
515
+ "search the rest"
516
+ )
495
517
  output = "".join(matches)
496
518
  if not output:
497
519
  raise VfsShellExit(1)
@@ -860,23 +882,28 @@ def _render_printf_template(template: str, values: list[str]) -> tuple[str, int]
860
882
  return "".join(output), used
861
883
 
862
884
 
863
- def _reject_bre_escapes(pattern: str) -> None:
885
+ def _translate_bre_escapes(pattern: str) -> str:
864
886
  """Patterns compile as Python (extended) regex, where BRE's operator escapes
865
- `\\|` `\\(` `\\)` mean the *literal* character — so a caller writing default-grep
866
- syntax like `foo\\|bar` would silently match nothing. Refuse loudly instead."""
887
+ `\\|` `\\(` `\\)` would mean the *literal* character — so a caller writing
888
+ default-grep syntax like `foo\\|bar` would silently match nothing. Both
889
+ dialects intend an operator there, so treat the escape as the operator;
890
+ `-F` remains the way to match the character literally."""
891
+ output: list[str] = []
867
892
  index = 0
868
- while index < len(pattern) - 1:
869
- if pattern[index] != "\\":
870
- index += 1
893
+ while index < len(pattern):
894
+ char = pattern[index]
895
+ if char == "\\" and index + 1 < len(pattern):
896
+ escaped = pattern[index + 1]
897
+ if escaped in "|()":
898
+ output.append(escaped)
899
+ else:
900
+ output.append(char)
901
+ output.append(escaped)
902
+ index += 2
871
903
  continue
872
- escaped = pattern[index + 1]
873
- if escaped in "|()":
874
- raise VfsShellError(
875
- f"pattern escape \\{escaped} is BRE syntax; patterns here are extended "
876
- f"regex — use {escaped} as the operator, or -F to match text literally",
877
- exit_code=2,
878
- )
879
- index += 2
904
+ output.append(char)
905
+ index += 1
906
+ return "".join(output)
880
907
 
881
908
 
882
909
  def _grep_text(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes