feat(py): mirage mcp over stdio, and invert the mypy allowlist
Two cleanup-plan items, both Python-side.
Item 29 -- `mirage mcp`. TypeScript shipped a six-tool stdio MCP server;
Python had none, so a pip-install user could not point Cursor or Claude
Desktop at a workspace and `mirage --help` differed by distribution.
Adding the entry point alone would have duplicated the tools, because
this side kept them private inside the Claude Agent SDK integration, so
the shared layer comes first:
- agents/tool_descriptions.py -- the six strings, one copy.
- agents/tool_operations.py -- MirageToolOperations, lifted out of the
SDK server's private _MirageTools.
- agents/file_version.py -- stale-write protection, which this side
lacked entirely. TS stamps stored bytes; here the stamp covers the
rendered bytes, because this read tool has always rendered and an
edit must search what the agent was actually shown.
- agents/mcp/server.py + cli/mcp.py -- the server and `mirage mcp`.
- server/workspace_config.py -- config discovery (candidates, env
names, walk up from cwd), which Python had nowhere, so every entry
point had to be handed an explicit path.
The server is the low-level MCP Server rather than FastMCP: FastMCP does
not forward a version, and TS advertises one. Handlers are bound methods,
not decorated closures, so nothing nests.
Item 28 -- the mypy allowlist. 54 modules opted *in* to annotation
checking against 1826, so the default was unchecked and every new file
joined the unchecked side. The default is now strict, with a list of
what is not yet annotated that only shrinks. 166 annotations cleared
along the way; the remainder is named module by module.
Two real defects surfaced by the annotations, neither of them typing:
- Workspace._original_open / _original_os were invented by assignment
in lifecycle.patch_process, so unpatch without a patch raised
AttributeError. Declared, and the restore is guarded.
- sed_generic declared a non-optional writer while its own docstring
and its `write_bytes is None` branch said otherwise; the builder
passes None whenever the backend cannot write.
Tests keep the PathSpec rule instead of full strict: measured, full
strict on python/tests is 2374 errors, of which 634 are `str` where a
pydantic field declares SecretStr -- which pydantic coerces at runtime --
and most of the rest is the monkeypatched-fake pattern CLAUDE.md
sanctions. The rule that is violated for real is PathSpec, 19 times, and
scripts/check_test_pathspec.py now holds that line. One of the 19 was a
latent AttributeError: tests/e2e passes a str to s3 write_bytes, which
reads .mount_path, and the test skips without a live versioned bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -160,5 +160,11 @@ repos:
|
||||
name: Type check (mypy)
|
||||
entry: bash -c 'cd python && uv run mypy'
|
||||
language: system
|
||||
files: ^python/(mirage/.*\.py|pyproject\.toml)$
|
||||
files: ^python/(mirage/.*\.py|tests/.*\.py|pyproject\.toml)$
|
||||
pass_filenames: false
|
||||
- id: py-test-pathspec
|
||||
name: PathSpec discipline (tests)
|
||||
entry: python3 scripts/check_test_pathspec.py
|
||||
language: system
|
||||
files: ^python/(mirage/.*\.py|tests/.*\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import shlex
|
||||
from collections.abc import Awaitable
|
||||
from posixpath import dirname
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
||||
try:
|
||||
from agno.tools import Toolkit
|
||||
@@ -27,6 +28,8 @@ from mirage.agents.io_text import io_to_str
|
||||
from mirage.bridge.sync import run_async_from_sync
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MirageToolkit(Toolkit):
|
||||
"""Agno toolkit backed by a Mirage Workspace.
|
||||
@@ -55,7 +58,7 @@ class MirageToolkit(Toolkit):
|
||||
async_tools=async_tools,
|
||||
**kwargs)
|
||||
|
||||
def _run(self, coro):
|
||||
def _run(self, coro: Awaitable[T]) -> T:
|
||||
return run_async_from_sync(coro)
|
||||
|
||||
# -- execute ---------------------------------------------------------
|
||||
|
||||
@@ -13,35 +13,11 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.agents.prompts import MIRAGE_SYSTEM_PROMPT, build_system_prompt
|
||||
|
||||
EXECUTE_DESCRIPTION = (
|
||||
"Run a shell-style command on the Mirage virtual filesystem. "
|
||||
"Supports cat, grep, find, head, tail, ls, wc, sort, uniq, tee, pipe, "
|
||||
"and any other Unix command on mounted resources (S3, disk, RAM, etc.). "
|
||||
"Files with no registered renderer, such as .parquet or .orc, read back "
|
||||
"as raw bytes.")
|
||||
|
||||
READ_DESCRIPTION = (
|
||||
"Read the contents of a file on the Mirage virtual filesystem. "
|
||||
"Returns line-numbered text. "
|
||||
"Optionally pass 'offset' (default 0) to start at a given line "
|
||||
"and 'limit' (default 2000) to cap the number of lines returned.")
|
||||
|
||||
WRITE_DESCRIPTION = (
|
||||
"Write content to a new file on the Mirage virtual filesystem. "
|
||||
"Fails if the file already exists; use edit to modify an existing file.")
|
||||
|
||||
EDIT_DESCRIPTION = (
|
||||
"Replace a string in an existing file on the Mirage virtual filesystem. "
|
||||
"Fails if old_string is not found or appears more than once. "
|
||||
"Pass replace_all=true (default false) to replace every occurrence.")
|
||||
|
||||
LS_DESCRIPTION = ("List files and directories at the given path "
|
||||
"on the Mirage virtual filesystem.")
|
||||
|
||||
GREP_DESCRIPTION = (
|
||||
"Search for a pattern in files on the Mirage virtual filesystem. "
|
||||
"Supports regex. Searches recursively under path.")
|
||||
from mirage.agents.tool_descriptions import EDIT_DESCRIPTION # yapf: disable
|
||||
from mirage.agents.tool_descriptions import (EXECUTE_DESCRIPTION,
|
||||
GREP_DESCRIPTION, LS_DESCRIPTION,
|
||||
READ_DESCRIPTION,
|
||||
WRITE_DESCRIPTION)
|
||||
|
||||
__all__ = [
|
||||
"MIRAGE_SYSTEM_PROMPT",
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import shlex
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
@@ -26,128 +25,74 @@ from mirage import __version__
|
||||
from mirage.agents.claude_agent_sdk.prompt import ( # yapf: disable
|
||||
EDIT_DESCRIPTION, EXECUTE_DESCRIPTION, GREP_DESCRIPTION, LS_DESCRIPTION,
|
||||
READ_DESCRIPTION, WRITE_DESCRIPTION)
|
||||
from mirage.agents.io_text import io_to_str
|
||||
from mirage.io.types import IOResult
|
||||
from mirage.agents.tool_operations import (DEFAULT_READ_LIMIT,
|
||||
MirageToolOperations, ToolResult)
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
|
||||
def _text(text: str) -> dict[str, Any]:
|
||||
return {"content": [{"type": "text", "text": text}]}
|
||||
|
||||
|
||||
def _error(text: str) -> dict[str, Any]:
|
||||
return {"content": [{"type": "text", "text": text}], "is_error": True}
|
||||
|
||||
|
||||
def _io_to_result(io: IOResult) -> dict[str, Any]:
|
||||
result = _text(io_to_str(io))
|
||||
if io.exit_code != 0:
|
||||
result["is_error"] = True
|
||||
return result
|
||||
def _to_sdk(result: ToolResult) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": result.text
|
||||
}]
|
||||
}
|
||||
if result.is_error:
|
||||
payload["is_error"] = True
|
||||
return payload
|
||||
|
||||
|
||||
class _MirageTools:
|
||||
"""Unpacks the SDK's argument dicts onto the shared operations.
|
||||
|
||||
def __init__(self, workspace: Workspace) -> None:
|
||||
self._ws = workspace
|
||||
Args:
|
||||
workspace (Workspace): The workspace to serve.
|
||||
stale_write_protection (bool): False lets an agent overwrite a
|
||||
file that changed since it read it.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
workspace: Workspace,
|
||||
stale_write_protection: bool = True) -> None:
|
||||
self._ops = MirageToolOperations(workspace, stale_write_protection)
|
||||
|
||||
async def execute_command(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||
io = await self._ws.execute(args["command"])
|
||||
return _io_to_result(io)
|
||||
|
||||
async def _ensure_parents(self, path: str) -> None:
|
||||
parts = [p for p in path.split("/") if p]
|
||||
ops = self._ws.ops
|
||||
current = ""
|
||||
for part in parts[:-1]:
|
||||
current += "/" + part
|
||||
try:
|
||||
await ops.mkdir(current)
|
||||
except FileExistsError:
|
||||
continue
|
||||
return _to_sdk(await self._ops.execute(args["command"]))
|
||||
|
||||
async def read(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||
path = args["path"]
|
||||
offset = int(args.get("offset", 0))
|
||||
limit = int(args.get("limit", 2000))
|
||||
ops = self._ws.ops
|
||||
try:
|
||||
data = await ops.read(path)
|
||||
except FileNotFoundError:
|
||||
return _error(f"Error: file '{path}' not found")
|
||||
except ValueError as exc:
|
||||
return _error(f"Error: {exc}")
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
lines = text.splitlines(keepends=True)
|
||||
sliced = lines[offset:offset + limit]
|
||||
numbered = [
|
||||
f"{i + offset + 1:>6}\t{line}" for i, line in enumerate(sliced)
|
||||
]
|
||||
return _text("".join(numbered))
|
||||
limit = int(args.get("limit", DEFAULT_READ_LIMIT))
|
||||
return _to_sdk(await self._ops.read(args["path"], offset, limit))
|
||||
|
||||
async def write(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||
path = args["path"]
|
||||
content = args["content"]
|
||||
ops = self._ws.ops
|
||||
try:
|
||||
await ops.stat(path)
|
||||
except FileNotFoundError:
|
||||
# missing file is the good path: creation may proceed
|
||||
pass
|
||||
else:
|
||||
return _error(f"Error: file '{path}' already exists")
|
||||
await self._ensure_parents(path)
|
||||
data = content.encode("utf-8") if isinstance(content, str) else content
|
||||
await ops.write(path, data)
|
||||
return _text(f"Written: {path}")
|
||||
return _to_sdk(await self._ops.write(args["path"], args["content"]))
|
||||
|
||||
async def edit(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||
path = args["path"]
|
||||
old_string = args["old_string"]
|
||||
new_string = args["new_string"]
|
||||
replace_all = bool(args.get("replace_all", False))
|
||||
ops = self._ws.ops
|
||||
try:
|
||||
data = await ops.read(path)
|
||||
except FileNotFoundError:
|
||||
return _error(f"Error: file '{path}' not found")
|
||||
content = data.decode("utf-8", errors="replace")
|
||||
count = content.count(old_string)
|
||||
if count == 0:
|
||||
return _error(f"Error: string not found in file: '{old_string}'")
|
||||
if count > 1 and not replace_all:
|
||||
return _error(
|
||||
f"Error: string appears {count} times. Pass replace_all=true")
|
||||
new_content = content.replace(
|
||||
old_string, new_string) if replace_all else content.replace(
|
||||
old_string, new_string, 1)
|
||||
await ops.write(path, new_content.encode("utf-8"))
|
||||
occurrences = count if replace_all else 1
|
||||
return _text(f"Edited: {path} ({occurrences} occurrence(s))")
|
||||
return _to_sdk(await self._ops.edit(args["path"], args["old_string"],
|
||||
args["new_string"], replace_all))
|
||||
|
||||
async def ls(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||
path = args["path"]
|
||||
io = await self._ws.execute(f"ls {shlex.quote(path)}")
|
||||
return _io_to_result(io)
|
||||
return _to_sdk(await self._ops.ls(args["path"]))
|
||||
|
||||
async def grep(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||
pattern = args["pattern"]
|
||||
path = args["path"]
|
||||
io = await self._ws.execute(
|
||||
f"grep -rn {shlex.quote(pattern)} {shlex.quote(path)}")
|
||||
return _text(io_to_str(io))
|
||||
return _to_sdk(await self._ops.grep(args["pattern"], args["path"]))
|
||||
|
||||
|
||||
def MirageServer(workspace: Workspace):
|
||||
def MirageServer(workspace: Workspace,
|
||||
stale_write_protection: bool = True) -> Any:
|
||||
"""Create an in-process Mirage server for the Claude Agent SDK.
|
||||
|
||||
Args:
|
||||
workspace (Workspace): The workspace to serve.
|
||||
stale_write_protection (bool): False lets an agent overwrite a
|
||||
file that changed since it read it.
|
||||
|
||||
Returns:
|
||||
An SDK server object to pass to ClaudeAgentOptions(mcp_servers=...).
|
||||
Any: An SDK server object to pass to
|
||||
ClaudeAgentOptions(mcp_servers=...).
|
||||
"""
|
||||
tools_impl = _MirageTools(workspace)
|
||||
tools_impl = _MirageTools(workspace, stale_write_protection)
|
||||
return create_sdk_mcp_server(
|
||||
name="mirage",
|
||||
version=__version__,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
|
||||
class StaleMirageFileError(Exception):
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
super().__init__(f"File changed since it was last read: {path}. "
|
||||
f"Read the file again before modifying it.")
|
||||
self.path = path
|
||||
|
||||
|
||||
def fingerprint(content: bytes) -> str:
|
||||
"""Version stamp for one file's stored bytes.
|
||||
|
||||
Args:
|
||||
content (bytes): The bytes to stamp.
|
||||
|
||||
Returns:
|
||||
str: A base64url digest, matching the TypeScript tracker's stamp.
|
||||
"""
|
||||
digest = hashlib.sha256(content).digest()
|
||||
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
||||
|
||||
|
||||
class FileVersionTracker:
|
||||
"""Refuses a write to a file that moved under the agent.
|
||||
|
||||
Two stamps are kept per path because a read and an edit are
|
||||
different promises: `read` records what the agent was shown, and
|
||||
`read_for_edit` records what it is about to rewrite. A plain write
|
||||
is checked against the read stamp, an edit against the edit stamp.
|
||||
|
||||
Stamps cover the rendered bytes, which is what the read tool hands
|
||||
the agent. The TypeScript tracker stamps the stored bytes instead;
|
||||
here that would let `edit` search bytes the agent never saw, since
|
||||
this side's read tool has always rendered.
|
||||
|
||||
Args:
|
||||
workspace (Workspace): The workspace to read and write through.
|
||||
enabled (bool): False serves every call unchecked, which is
|
||||
what `mirage mcp --no-stale-write-protection` asks for.
|
||||
"""
|
||||
|
||||
def __init__(self, workspace: Workspace, enabled: bool = True) -> None:
|
||||
self._ws = workspace
|
||||
self._enabled = enabled
|
||||
self._read_versions: dict[str, str] = {}
|
||||
self._edit_versions: dict[str, str] = {}
|
||||
|
||||
async def _current_version(self, path: str) -> str | None:
|
||||
if not await self._ws.ops.exists(path):
|
||||
return None
|
||||
return fingerprint(await self._ws.ops.read(path))
|
||||
|
||||
async def _assert_version(self, path: str, expected: str) -> None:
|
||||
if await self._current_version(path) != expected:
|
||||
raise StaleMirageFileError(path)
|
||||
|
||||
def _record_write(self, path: str, content: str) -> None:
|
||||
if not self._enabled:
|
||||
return
|
||||
self._read_versions[path] = fingerprint(content.encode("utf-8"))
|
||||
self._edit_versions.pop(path, None)
|
||||
|
||||
async def read(self, path: str) -> bytes:
|
||||
"""Read a file and record what the agent was shown.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
|
||||
Returns:
|
||||
bytes: The stored bytes.
|
||||
"""
|
||||
content = await self._ws.ops.read(path)
|
||||
if self._enabled:
|
||||
self._read_versions[path] = fingerprint(content)
|
||||
return content
|
||||
|
||||
async def read_for_edit(self, path: str) -> bytes:
|
||||
"""Read a file the agent is about to rewrite.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
|
||||
Returns:
|
||||
bytes: The stored bytes.
|
||||
|
||||
Raises:
|
||||
StaleMirageFileError: The file moved since it was last read.
|
||||
"""
|
||||
content = await self._ws.ops.read(path)
|
||||
if not self._enabled:
|
||||
return content
|
||||
version = fingerprint(content)
|
||||
read_version = self._read_versions.get(path)
|
||||
if read_version is not None and read_version != version:
|
||||
raise StaleMirageFileError(path)
|
||||
self._edit_versions[path] = version
|
||||
return content
|
||||
|
||||
async def write(self, path: str, content: str) -> None:
|
||||
"""Write a file, refusing if it moved since it was read.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
content (str): Text to write.
|
||||
|
||||
Raises:
|
||||
StaleMirageFileError: The file moved since it was last read.
|
||||
"""
|
||||
if self._enabled:
|
||||
read_version = self._read_versions.get(path)
|
||||
if read_version is not None:
|
||||
await self._assert_version(path, read_version)
|
||||
await self._ws.ops.write(path, content.encode("utf-8"))
|
||||
self._record_write(path, content)
|
||||
|
||||
async def write_edit(self, path: str, content: str) -> None:
|
||||
"""Write an edit, refusing if it moved since it was read for edit.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
content (str): Text to write.
|
||||
|
||||
Raises:
|
||||
StaleMirageFileError: The file moved since it was read for edit.
|
||||
"""
|
||||
if self._enabled:
|
||||
edit_version = self._edit_versions.get(path)
|
||||
if edit_version is not None:
|
||||
await self._assert_version(path, edit_version)
|
||||
await self._ws.ops.write(path, content.encode("utf-8"))
|
||||
self._record_write(path, content)
|
||||
@@ -14,7 +14,9 @@
|
||||
|
||||
import base64
|
||||
import shlex
|
||||
from collections.abc import Awaitable
|
||||
from pathlib import PurePosixPath
|
||||
from typing import TypeVar
|
||||
|
||||
from deepagents.backends.protocol import (EditResult, ExecuteResponse,
|
||||
FileData, FileDownloadResponse,
|
||||
@@ -30,6 +32,8 @@ from mirage.bridge.sync import run_async_from_sync
|
||||
from mirage.io.types import IOResult
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
BINARY_EXTENSIONS = frozenset({
|
||||
".3gpp",
|
||||
".aac",
|
||||
@@ -119,7 +123,7 @@ class LangchainWorkspace(SandboxBackendProtocol):
|
||||
self._id = sandbox_id
|
||||
self._session_id = session_id
|
||||
|
||||
def _run(self, coro):
|
||||
def _run(self, coro: Awaitable[T]) -> T:
|
||||
return run_async_from_sync(coro)
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.agents.mcp.server import (MirageMcpServer,
|
||||
create_mirage_mcp_server,
|
||||
serve_mirage_mcp)
|
||||
|
||||
__all__ = [
|
||||
"MirageMcpServer",
|
||||
"create_mirage_mcp_server",
|
||||
"serve_mirage_mcp",
|
||||
]
|
||||
@@ -0,0 +1,235 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import CallToolResult, TextContent, Tool, ToolAnnotations
|
||||
|
||||
from mirage import __version__
|
||||
from mirage.agents.tool_descriptions import EDIT_DESCRIPTION # yapf: disable
|
||||
from mirage.agents.tool_descriptions import (EXECUTE_DESCRIPTION,
|
||||
GREP_DESCRIPTION, LS_DESCRIPTION,
|
||||
READ_DESCRIPTION,
|
||||
WRITE_DESCRIPTION)
|
||||
from mirage.agents.tool_operations import (DEFAULT_READ_LIMIT,
|
||||
MirageToolOperations, ToolResult)
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
READ_ONLY = ToolAnnotations(readOnlyHint=True)
|
||||
|
||||
TOOLS = [
|
||||
Tool(name="execute_command",
|
||||
description=EXECUTE_DESCRIPTION,
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
}),
|
||||
Tool(name="read",
|
||||
description=READ_DESCRIPTION,
|
||||
annotations=READ_ONLY,
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
},
|
||||
"required": ["path"],
|
||||
}),
|
||||
Tool(name="write",
|
||||
description=WRITE_DESCRIPTION,
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
}),
|
||||
Tool(name="edit",
|
||||
description=EDIT_DESCRIPTION,
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string"
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string"
|
||||
},
|
||||
"replace_all": {
|
||||
"type": "boolean"
|
||||
},
|
||||
},
|
||||
"required": ["path", "old_string", "new_string"],
|
||||
}),
|
||||
Tool(name="ls",
|
||||
description=LS_DESCRIPTION,
|
||||
annotations=READ_ONLY,
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
}),
|
||||
Tool(name="grep",
|
||||
description=GREP_DESCRIPTION,
|
||||
annotations=READ_ONLY,
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
},
|
||||
"required": ["pattern", "path"],
|
||||
}),
|
||||
]
|
||||
|
||||
|
||||
def _to_mcp(result: ToolResult) -> CallToolResult:
|
||||
return CallToolResult(content=[TextContent(type="text", text=result.text)],
|
||||
isError=result.is_error)
|
||||
|
||||
|
||||
class MirageMcpServer:
|
||||
"""Serves one workspace's six tools over the MCP protocol.
|
||||
|
||||
The handlers are bound methods rather than decorated closures, so
|
||||
the tool table stays readable and nothing nests.
|
||||
|
||||
Args:
|
||||
workspace (Workspace): The workspace to serve.
|
||||
stale_write_protection (bool): False lets an agent overwrite a
|
||||
file that changed since it read it.
|
||||
name (str): Server name advertised to the client.
|
||||
version (str): Server version advertised to the client.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
workspace: Workspace,
|
||||
stale_write_protection: bool = True,
|
||||
name: str = "mirage",
|
||||
version: str = __version__) -> None:
|
||||
self._ops = MirageToolOperations(workspace, stale_write_protection)
|
||||
self.server: Server[object, object] = Server(name, version=version)
|
||||
self.server.list_tools()(self.list_tools)
|
||||
self.server.call_tool()(self.call_tool)
|
||||
|
||||
async def list_tools(self) -> list[Tool]:
|
||||
"""Report the tool table.
|
||||
|
||||
Returns:
|
||||
list[Tool]: Every tool this server serves.
|
||||
"""
|
||||
return list(TOOLS)
|
||||
|
||||
async def call_tool(self, name: str,
|
||||
arguments: dict[str, Any]) -> CallToolResult:
|
||||
"""Run one tool call.
|
||||
|
||||
Args:
|
||||
name (str): The tool being called.
|
||||
arguments (dict[str, Any]): Arguments, already validated
|
||||
against the tool's input schema.
|
||||
|
||||
Returns:
|
||||
CallToolResult: The tool's answer.
|
||||
|
||||
Raises:
|
||||
ValueError: The tool name is not one this server serves.
|
||||
"""
|
||||
if name == "execute_command":
|
||||
return _to_mcp(await self._ops.execute(arguments["command"]))
|
||||
if name == "read":
|
||||
return _to_mcp(await self._ops.read(
|
||||
arguments["path"], int(arguments.get("offset", 0)),
|
||||
int(arguments.get("limit", DEFAULT_READ_LIMIT))))
|
||||
if name == "write":
|
||||
return _to_mcp(await self._ops.write(arguments["path"],
|
||||
arguments["content"]))
|
||||
if name == "edit":
|
||||
return _to_mcp(await self._ops.edit(
|
||||
arguments["path"], arguments["old_string"],
|
||||
arguments["new_string"],
|
||||
bool(arguments.get("replace_all", False))))
|
||||
if name == "ls":
|
||||
return _to_mcp(await self._ops.ls(arguments["path"]))
|
||||
if name == "grep":
|
||||
return _to_mcp(await self._ops.grep(arguments["pattern"],
|
||||
arguments["path"]))
|
||||
raise ValueError(f"unknown tool: {name}")
|
||||
|
||||
async def run_stdio(self) -> None:
|
||||
"""Serve the workspace over stdio until the client disconnects."""
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await self.server.run(read_stream, write_stream,
|
||||
self.server.create_initialization_options())
|
||||
|
||||
|
||||
def create_mirage_mcp_server(
|
||||
workspace: Workspace,
|
||||
stale_write_protection: bool = True) -> MirageMcpServer:
|
||||
"""Build an MCP server for a workspace without serving it.
|
||||
|
||||
Args:
|
||||
workspace (Workspace): The workspace to serve.
|
||||
stale_write_protection (bool): False lets an agent overwrite a
|
||||
file that changed since it read it.
|
||||
|
||||
Returns:
|
||||
MirageMcpServer: The unserved server.
|
||||
"""
|
||||
return MirageMcpServer(workspace, stale_write_protection)
|
||||
|
||||
|
||||
async def serve_mirage_mcp(workspace: Workspace,
|
||||
stale_write_protection: bool = True) -> None:
|
||||
"""Serve a workspace as MCP tools over stdio.
|
||||
|
||||
Args:
|
||||
workspace (Workspace): The workspace to serve.
|
||||
stale_write_protection (bool): False lets an agent overwrite a
|
||||
file that changed since it read it.
|
||||
"""
|
||||
await create_mirage_mcp_server(workspace,
|
||||
stale_write_protection).run_stdio()
|
||||
@@ -13,6 +13,8 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import shlex
|
||||
from collections.abc import Awaitable
|
||||
from typing import TypeVar
|
||||
|
||||
from pydantic_ai_backends.protocol import SandboxProtocol
|
||||
from pydantic_ai_backends.types import (EditResult, ExecuteResponse, FileInfo,
|
||||
@@ -25,6 +27,8 @@ from mirage.bridge.sync import run_async_from_sync
|
||||
from mirage.io.types import IOResult
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class PydanticAIWorkspace(SandboxProtocol):
|
||||
"""Pydantic AI backend backed by a Mirage Workspace.
|
||||
@@ -44,7 +48,7 @@ class PydanticAIWorkspace(SandboxProtocol):
|
||||
self._id = sandbox_id
|
||||
self._session_id = session_id
|
||||
|
||||
def _run(self, coro):
|
||||
def _run(self, coro: Awaitable[T]) -> T:
|
||||
return run_async_from_sync(coro)
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
EXECUTE_DESCRIPTION = (
|
||||
"Run a shell-style command on the Mirage virtual filesystem. "
|
||||
"Supports cat, grep, find, head, tail, ls, wc, sort, uniq, tee, pipe, "
|
||||
"and any other Unix command on mounted resources (S3, disk, RAM, etc.). "
|
||||
"Files with no registered renderer, such as .parquet or .orc, read back "
|
||||
"as raw bytes.")
|
||||
|
||||
READ_DESCRIPTION = (
|
||||
"Read the contents of a file on the Mirage virtual filesystem. "
|
||||
"Returns line-numbered text. "
|
||||
"Optionally pass 'offset' (default 0) to start at a given line "
|
||||
"and 'limit' (default 2000) to cap the number of lines returned.")
|
||||
|
||||
WRITE_DESCRIPTION = (
|
||||
"Write content to a new file on the Mirage virtual filesystem. "
|
||||
"Fails if the file already exists; use edit to modify an existing file.")
|
||||
|
||||
EDIT_DESCRIPTION = (
|
||||
"Replace a string in an existing file on the Mirage virtual filesystem. "
|
||||
"Fails if the file changed since it was last read, old_string is not "
|
||||
"found, or old_string appears more than once. "
|
||||
"Pass replace_all=true (default false) to replace every occurrence.")
|
||||
|
||||
LS_DESCRIPTION = ("List files and directories at the given path "
|
||||
"on the Mirage virtual filesystem.")
|
||||
|
||||
GREP_DESCRIPTION = (
|
||||
"Search for a pattern in files on the Mirage virtual filesystem. "
|
||||
"Supports regex. Searches recursively under path.")
|
||||
|
||||
__all__ = [
|
||||
"EXECUTE_DESCRIPTION",
|
||||
"READ_DESCRIPTION",
|
||||
"WRITE_DESCRIPTION",
|
||||
"EDIT_DESCRIPTION",
|
||||
"LS_DESCRIPTION",
|
||||
"GREP_DESCRIPTION",
|
||||
]
|
||||
@@ -0,0 +1,224 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import shlex
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mirage.agents.file_version import FileVersionTracker, StaleMirageFileError
|
||||
from mirage.agents.io_text import decode, io_to_str
|
||||
from mirage.io.types import IOResult
|
||||
from mirage.utils.path import gnu_dirname
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
DEFAULT_READ_LIMIT = 2000
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ToolResult:
|
||||
"""One tool's answer, before any framework's result shape.
|
||||
|
||||
The two servers spell the failure flag differently -- MCP puts
|
||||
`isError` on the wire, the Claude Agent SDK takes `is_error` -- so
|
||||
the shared layer carries the fact and each server renders it.
|
||||
|
||||
Args:
|
||||
text (str): The text handed back to the agent.
|
||||
is_error (bool): True when the call failed.
|
||||
"""
|
||||
|
||||
text: str
|
||||
is_error: bool = False
|
||||
|
||||
|
||||
def _io_result(io: IOResult) -> ToolResult:
|
||||
return ToolResult(io_to_str(io), io.exit_code != 0)
|
||||
|
||||
|
||||
def number_lines(text: str, offset: int, limit: int) -> str:
|
||||
"""Render a slice of a file the way the read tool reports it.
|
||||
|
||||
Splits on newlines only. `str.splitlines` would also break on
|
||||
\\v, \\f, \\x1c-\\x1e, \\x85 and the Unicode separators, which
|
||||
would number a file containing any of them differently from the
|
||||
TypeScript tool.
|
||||
|
||||
Args:
|
||||
text (str): The decoded file content.
|
||||
offset (int): First line to show, zero-based.
|
||||
limit (int): Maximum number of lines to show.
|
||||
|
||||
Returns:
|
||||
str: The numbered lines, joined.
|
||||
"""
|
||||
if not text:
|
||||
lines: list[str] = []
|
||||
else:
|
||||
parts = text.split("\n")
|
||||
lines = [part + "\n" for part in parts[:-1]]
|
||||
if parts[-1]:
|
||||
lines.append(parts[-1])
|
||||
sliced = lines[offset:offset + limit]
|
||||
return "".join(f"{i + offset + 1:>6}\t{line}"
|
||||
for i, line in enumerate(sliced))
|
||||
|
||||
|
||||
async def ensure_parents(ws: Workspace, path: str) -> None:
|
||||
"""Create the directories a new file needs, parents first.
|
||||
|
||||
Args:
|
||||
ws (Workspace): The workspace to create them in.
|
||||
path (str): Virtual path of the file about to be written.
|
||||
"""
|
||||
parent = gnu_dirname(path)
|
||||
if parent in ("/", "", "."):
|
||||
return
|
||||
if await ws.ops.exists(parent):
|
||||
return
|
||||
await ensure_parents(ws, parent)
|
||||
try:
|
||||
await ws.ops.mkdir(parent)
|
||||
except OSError:
|
||||
if not await ws.ops.exists(parent):
|
||||
raise
|
||||
|
||||
|
||||
class MirageToolOperations:
|
||||
"""The six agent tools, independent of any agent framework.
|
||||
|
||||
Args:
|
||||
workspace (Workspace): The workspace the tools act on.
|
||||
stale_write_protection (bool): False lets an agent overwrite a
|
||||
file that changed since it read it.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
workspace: Workspace,
|
||||
stale_write_protection: bool = True) -> None:
|
||||
self._ws = workspace
|
||||
self._versions = FileVersionTracker(workspace, stale_write_protection)
|
||||
|
||||
async def execute(self, command: str) -> ToolResult:
|
||||
"""Run a shell-style command line.
|
||||
|
||||
Args:
|
||||
command (str): The command line to run.
|
||||
|
||||
Returns:
|
||||
ToolResult: The command's rendered output.
|
||||
"""
|
||||
return _io_result(await self._ws.execute(command))
|
||||
|
||||
async def read(self,
|
||||
path: str,
|
||||
offset: int = 0,
|
||||
limit: int = DEFAULT_READ_LIMIT) -> ToolResult:
|
||||
"""Read a file as line-numbered text.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
offset (int): First line to show, zero-based.
|
||||
limit (int): Maximum number of lines to show.
|
||||
|
||||
Returns:
|
||||
ToolResult: The numbered lines, or the failure.
|
||||
"""
|
||||
try:
|
||||
data = await self._versions.read(path)
|
||||
except (OSError, ValueError) as exc:
|
||||
if not await self._ws.ops.exists(path):
|
||||
return ToolResult(f"Error: file '{path}' not found", True)
|
||||
return ToolResult(f"Error: {exc}", True)
|
||||
return ToolResult(number_lines(decode(data), offset, limit))
|
||||
|
||||
async def write(self, path: str, content: str) -> ToolResult:
|
||||
"""Create a file, refusing to clobber an existing one.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
content (str): Text to write.
|
||||
|
||||
Returns:
|
||||
ToolResult: The confirmation, or the failure.
|
||||
"""
|
||||
if await self._ws.ops.exists(path):
|
||||
return ToolResult(f"Error: file '{path}' already exists", True)
|
||||
await ensure_parents(self._ws, path)
|
||||
await self._versions.write(path, content)
|
||||
return ToolResult(f"Written: {path}")
|
||||
|
||||
async def edit(self,
|
||||
path: str,
|
||||
old_string: str,
|
||||
new_string: str,
|
||||
replace_all: bool = False) -> ToolResult:
|
||||
"""Replace a string in an existing file.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
old_string (str): The text to find.
|
||||
new_string (str): The text to put in its place.
|
||||
replace_all (bool): True replaces every occurrence.
|
||||
|
||||
Returns:
|
||||
ToolResult: The confirmation, or the failure.
|
||||
"""
|
||||
try:
|
||||
content = decode(await self._versions.read_for_edit(path))
|
||||
except StaleMirageFileError as exc:
|
||||
return ToolResult(f"Error: {exc}", True)
|
||||
except (OSError, ValueError) as exc:
|
||||
if not await self._ws.ops.exists(path):
|
||||
return ToolResult(f"Error: file '{path}' not found", True)
|
||||
return ToolResult(f"Error: {exc}", True)
|
||||
count = content.count(old_string)
|
||||
if count == 0:
|
||||
return ToolResult(
|
||||
f"Error: string not found in file: '{old_string}'", True)
|
||||
if count > 1 and not replace_all:
|
||||
return ToolResult(
|
||||
f"Error: string appears {count} times. Pass replace_all=true",
|
||||
True)
|
||||
new_content = content.replace(old_string, new_string,
|
||||
-1 if replace_all else 1)
|
||||
try:
|
||||
await self._versions.write_edit(path, new_content)
|
||||
except StaleMirageFileError as exc:
|
||||
return ToolResult(f"Error: {exc}", True)
|
||||
occurrences = count if replace_all else 1
|
||||
return ToolResult(f"Edited: {path} ({occurrences} occurrence(s))")
|
||||
|
||||
async def ls(self, path: str) -> ToolResult:
|
||||
"""List a directory.
|
||||
|
||||
Args:
|
||||
path (str): Virtual path.
|
||||
|
||||
Returns:
|
||||
ToolResult: The listing, or the failure.
|
||||
"""
|
||||
return _io_result(await self._ws.execute(f"ls {shlex.quote(path)}"))
|
||||
|
||||
async def grep(self, pattern: str, path: str) -> ToolResult:
|
||||
"""Search recursively for a pattern.
|
||||
|
||||
Args:
|
||||
pattern (str): The regex to search for.
|
||||
path (str): Virtual path to search under.
|
||||
|
||||
Returns:
|
||||
ToolResult: The matches.
|
||||
"""
|
||||
io = await self._ws.execute(
|
||||
f"grep -rn {shlex.quote(pattern)} {shlex.quote(path)}")
|
||||
return ToolResult(io_to_str(io))
|
||||
@@ -18,6 +18,7 @@ from mirage.cli import config as config_module
|
||||
from mirage.cli import daemon as daemon_module
|
||||
from mirage.cli import execute as execute_module
|
||||
from mirage.cli import job as job_module
|
||||
from mirage.cli import mcp as mcp_module
|
||||
from mirage.cli import provision as provision_module
|
||||
from mirage.cli import session as session_module
|
||||
from mirage.cli import workspace as workspace_module
|
||||
@@ -35,6 +36,7 @@ app.add_typer(execute_module.app, name="execute")
|
||||
app.add_typer(provision_module.app, name="provision")
|
||||
app.add_typer(daemon_module.app, name="daemon")
|
||||
app.add_typer(config_module.app, name="config")
|
||||
app.add_typer(mcp_module.app, name="mcp")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from mirage.agents.mcp.server import serve_mirage_mcp
|
||||
from mirage.server.workspace_config import (build_workspace_from_config,
|
||||
resolve_workspace_config)
|
||||
|
||||
MCP_ENV_NAMES = ("MIRAGE_MCP_CONFIG", "MIRAGE_CONFIG")
|
||||
|
||||
app = typer.Typer(invoke_without_command=True,
|
||||
help="Serve a Mirage workspace as MCP tools over stdio.")
|
||||
|
||||
|
||||
def resolve_mcp_config(config: str | None = None,
|
||||
cwd: str | Path | None = None,
|
||||
env: dict[str, str] | None = None) -> Path:
|
||||
"""Find the config `mirage mcp` should serve.
|
||||
|
||||
Args:
|
||||
config (str | None): explicit path, relative to cwd.
|
||||
cwd (str | Path | None): directory to resolve from.
|
||||
env (dict[str, str] | None): environment mapping to read.
|
||||
|
||||
Returns:
|
||||
Path: the resolved config path.
|
||||
"""
|
||||
return resolve_workspace_config(config,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
env_names=MCP_ENV_NAMES)
|
||||
|
||||
|
||||
async def run_mcp_server(config: str | None,
|
||||
stale_write_protection: bool) -> None:
|
||||
"""Build the workspace and serve it until the client disconnects.
|
||||
|
||||
Args:
|
||||
config (str | None): explicit config path, or None to discover.
|
||||
stale_write_protection (bool): False lets an agent overwrite a
|
||||
file that changed since it read it.
|
||||
"""
|
||||
workspace = await build_workspace_from_config(resolve_mcp_config(config))
|
||||
try:
|
||||
await serve_mirage_mcp(workspace, stale_write_protection)
|
||||
finally:
|
||||
await workspace.close()
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def mcp_cmd(
|
||||
config: str | None = typer.Argument(None,
|
||||
help="Mirage workspace YAML config."),
|
||||
stale_write_protection: bool = typer.Option(
|
||||
True,
|
||||
"--stale-write-protection/--no-stale-write-protection",
|
||||
help="Refuse an edit when the file changed since it was read."),
|
||||
) -> None:
|
||||
"""Serve a Mirage workspace as MCP tools over stdio."""
|
||||
try:
|
||||
asyncio.run(run_mcp_server(config, stale_write_protection))
|
||||
except FileNotFoundError as e:
|
||||
typer.echo(str(e), err=True)
|
||||
raise SystemExit(2) from e
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import replace
|
||||
from functools import partial
|
||||
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.commands.builtin.chroma.io import resolve_glob
|
||||
from mirage.commands.builtin.generic.find import find_generic
|
||||
from mirage.commands.builtin.utils.output import format_records
|
||||
@@ -52,7 +53,7 @@ async def _normalize_find_output(
|
||||
|
||||
@command("find", resource="chroma", spec=SPECS["find"])
|
||||
async def find(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.commands.builtin.chroma.io import resolve_glob
|
||||
from mirage.commands.builtin.utils.paths import default_paths
|
||||
from mirage.commands.config import CommandOpts
|
||||
@@ -18,7 +19,7 @@ def is_mount_root(path: PathSpec) -> bool:
|
||||
|
||||
@command("chroma-query", resource="chroma", spec=SPECS["search"])
|
||||
async def search(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.dify import DifyAccessor
|
||||
from mirage.commands.builtin.generic.cat import cat_generic
|
||||
from mirage.commands.builtin.generic_bind import CommandIO
|
||||
from mirage.commands.builtin.generic_bind.adapter import (bound_op,
|
||||
@@ -20,8 +21,12 @@ def make_cat(ops: CommandIO):
|
||||
"""
|
||||
|
||||
@command("cat", resource="dify", spec=SPECS["cat"])
|
||||
async def cat(accessor, paths: list[PathSpec], texts: list[str],
|
||||
opts: CommandOpts) -> tuple[ByteSource | None, IOResult]:
|
||||
async def cat(
|
||||
accessor: DifyAccessor,
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
resolved = await resolve_or_empty(ops, accessor, paths, opts.index)
|
||||
return await cat_generic(resolved,
|
||||
list(texts),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import replace
|
||||
from functools import partial
|
||||
|
||||
from mirage.accessor.dify import DifyAccessor
|
||||
from mirage.commands.builtin.dify.io import resolve_glob
|
||||
from mirage.commands.builtin.generic.find import find_generic
|
||||
from mirage.commands.builtin.utils.output import format_records
|
||||
@@ -51,8 +52,12 @@ async def _normalize_find_output(
|
||||
|
||||
|
||||
@command("find", resource="dify", spec=SPECS["find"])
|
||||
async def find(accessor, paths: list[PathSpec], texts: list[str],
|
||||
opts: CommandOpts) -> tuple[ByteSource | None, IOResult]:
|
||||
async def find(
|
||||
accessor: DifyAccessor,
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
paths = default_paths(paths, opts.cwd)
|
||||
paths = await resolve_glob(accessor, paths, opts.index)
|
||||
search_path = paths[0]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.dify import DifyAccessor
|
||||
from mirage.commands.builtin.dify.io import resolve_glob
|
||||
from mirage.commands.builtin.utils.paths import default_paths
|
||||
from mirage.commands.config import CommandOpts
|
||||
@@ -18,7 +19,7 @@ def is_mount_root(path: PathSpec) -> bool:
|
||||
|
||||
@command("search", resource="dify", spec=SPECS["search"])
|
||||
async def search(
|
||||
accessor,
|
||||
accessor: DifyAccessor,
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
|
||||
@@ -8,7 +8,7 @@ from mirage.commands.spec import SPECS
|
||||
from mirage.commands.spec.types import CommandName, FlagValue, FlagView
|
||||
from mirage.commands.spec.usage import extra_operand_error
|
||||
from mirage.io.types import ByteSource, IOResult
|
||||
from mirage.types import PathSpec
|
||||
from mirage.types import PathSpec, ReadStreamFn
|
||||
|
||||
|
||||
async def _base64_encode_stream(source: AsyncIterator[bytes],
|
||||
@@ -83,7 +83,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> Base64Flags:
|
||||
)
|
||||
|
||||
|
||||
async def base64_generic(paths, texts, opts: CommandOpts, read_stream):
|
||||
async def base64_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_stream: ReadStreamFn,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await base64_cmd(paths,
|
||||
read_stream=read_stream,
|
||||
|
||||
@@ -91,7 +91,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> CmpFlags:
|
||||
)
|
||||
|
||||
|
||||
async def cmp_generic(paths, texts, opts: CommandOpts, read_bytes):
|
||||
async def cmp_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await cmp_cmd(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -83,7 +83,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> ColumnFlags:
|
||||
)
|
||||
|
||||
|
||||
async def column_generic(paths, texts, opts: CommandOpts, read_bytes):
|
||||
async def column_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await column(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -134,7 +134,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> CommFlags:
|
||||
)
|
||||
|
||||
|
||||
async def comm_generic(paths, texts, opts: CommandOpts, read_bytes):
|
||||
async def comm_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await comm(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -10,7 +10,7 @@ from mirage.commands.spec import SPECS
|
||||
from mirage.commands.spec.types import FlagValue, FlagView
|
||||
from mirage.io.types import ByteSource, IOResult
|
||||
from mirage.ops.types import LinkView
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, PathSpec
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, PathSpec, StatFn
|
||||
from mirage.utils.path import CycleError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -107,7 +107,13 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> FileFlags:
|
||||
return FileFlags(brief=fl.as_bool("b"), mime=fl.as_bool("i"))
|
||||
|
||||
|
||||
async def file_generic(paths, texts, opts: CommandOpts, read_bytes, stat_fn):
|
||||
async def file_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
stat_fn: StatFn,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await file_cmd(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -87,8 +87,14 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> GunzipFlags:
|
||||
)
|
||||
|
||||
|
||||
async def gunzip_generic(paths, texts, opts: CommandOpts, read_bytes,
|
||||
write_bytes, unlink):
|
||||
async def gunzip_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]],
|
||||
unlink: Callable[..., Awaitable[None]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await gunzip(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -129,8 +129,14 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> GzipFlags:
|
||||
)
|
||||
|
||||
|
||||
async def gzip_generic(paths, texts, opts: CommandOpts, read_bytes,
|
||||
write_bytes, unlink):
|
||||
async def gzip_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]],
|
||||
unlink: Callable[..., Awaitable[None]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await gzip(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -56,8 +56,13 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> IconvFlags:
|
||||
)
|
||||
|
||||
|
||||
async def iconv_generic(paths, texts, opts: CommandOpts, read_bytes,
|
||||
write_bytes):
|
||||
async def iconv_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await iconv(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -232,7 +232,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> JoinFlags:
|
||||
)
|
||||
|
||||
|
||||
async def join_generic(paths, texts, opts: CommandOpts, read_bytes):
|
||||
async def join_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await join_cmd(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -52,7 +52,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> LookFlags:
|
||||
return LookFlags(fold_case=fl.as_bool("f"))
|
||||
|
||||
|
||||
async def look_generic(paths, texts, opts: CommandOpts, read_bytes):
|
||||
async def look_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
if not texts:
|
||||
raise ValueError("look: missing prefix")
|
||||
parsed = parse_flags(opts.flags)
|
||||
|
||||
@@ -186,8 +186,14 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> PatchFlags:
|
||||
)
|
||||
|
||||
|
||||
async def patch_generic(paths, texts, opts: CommandOpts, read_bytes,
|
||||
write_bytes, has_resource):
|
||||
async def patch_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]],
|
||||
has_resource: bool,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await patch(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -53,7 +53,11 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> ReadlinkFlags:
|
||||
)
|
||||
|
||||
|
||||
async def readlink_generic(paths, texts, opts: CommandOpts):
|
||||
async def readlink_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
if not paths:
|
||||
raise ValueError("readlink: missing operand")
|
||||
parsed = parse_flags(opts.flags)
|
||||
|
||||
@@ -58,7 +58,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> RealpathFlags:
|
||||
allow_missing=fl.as_bool("m"))
|
||||
|
||||
|
||||
async def realpath_generic(paths, texts, opts: CommandOpts, stat_fn):
|
||||
async def realpath_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
stat_fn: StatFn,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await realpath(paths,
|
||||
stat_fn=stat_fn,
|
||||
|
||||
@@ -198,8 +198,14 @@ def _positional_as_paths(texts: list[str],
|
||||
return out
|
||||
|
||||
|
||||
async def sed_generic(paths, texts, opts: CommandOpts, resolve_glob,
|
||||
read_bytes, write_bytes):
|
||||
async def sed_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
resolve_glob: Callable[..., Awaitable[list[PathSpec]]],
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]] | None,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
"""Run sed over the given operands; mirrors sedGeneric.
|
||||
|
||||
The script comes from -e expressions and -f script files (joined
|
||||
|
||||
@@ -10,7 +10,7 @@ from mirage.commands.spec.types import FlagValue, FlagView
|
||||
from mirage.core.timeutil import iso_to_epoch
|
||||
from mirage.io.types import ByteSource, IOResult
|
||||
from mirage.ops.types import LinkView
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, PathSpec
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, PathSpec, StatFn
|
||||
from mirage.utils.errors import FS_ERRORS, fs_error_line
|
||||
|
||||
_STR_DIRECTIVES = frozenset("nNF")
|
||||
@@ -427,7 +427,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> StatFlags:
|
||||
)
|
||||
|
||||
|
||||
async def stat_generic(paths, texts, opts: CommandOpts, stat_fn):
|
||||
async def stat_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
stat_fn: StatFn,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await stat(paths,
|
||||
stat_fn=stat_fn,
|
||||
|
||||
@@ -246,8 +246,17 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> TarFlags:
|
||||
)
|
||||
|
||||
|
||||
async def tar_generic(paths, texts, opts: CommandOpts, read_bytes, write_bytes,
|
||||
mkdir_fn, stat, walk, is_dir):
|
||||
async def tar_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]],
|
||||
mkdir_fn: Callable[..., Awaitable[None]],
|
||||
stat: StatFn,
|
||||
walk: WalkFn,
|
||||
is_dir: DirProbe,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await tar(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -9,9 +9,9 @@ from mirage.commands.config import CommandOpts
|
||||
from mirage.commands.spec import SPECS
|
||||
from mirage.commands.spec.types import FlagValue, FlagView
|
||||
from mirage.context import mount_allowed
|
||||
from mirage.io.types import IOResult
|
||||
from mirage.io.types import ByteSource, IOResult
|
||||
from mirage.ops.types import MountView, ReaddirPath, StatPath
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import FileStat, FileType, PathSpec, ReaddirFn
|
||||
from mirage.utils.errors import WALK_ERRORS
|
||||
from mirage.utils.fnmatch import fnmatch
|
||||
from mirage.utils.key_prefix import rekey
|
||||
@@ -327,7 +327,13 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> TreeFlags:
|
||||
)
|
||||
|
||||
|
||||
async def tree_generic(paths, texts, opts: CommandOpts, readdir, stat):
|
||||
async def tree_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
readdir: ReaddirFn,
|
||||
stat: Stat,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await tree(paths[0],
|
||||
readdir=readdir,
|
||||
|
||||
@@ -187,8 +187,14 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> UnzipFlags:
|
||||
)
|
||||
|
||||
|
||||
async def unzip_generic(paths, texts, opts: CommandOpts, read_bytes,
|
||||
write_bytes, mkdir_fn):
|
||||
async def unzip_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]],
|
||||
mkdir_fn: Callable[..., Awaitable[None]],
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await unzip(paths,
|
||||
read_bytes=read_bytes,
|
||||
|
||||
@@ -10,7 +10,7 @@ from mirage.commands.spec import SPECS
|
||||
from mirage.commands.spec.types import CommandName, FlagValue, FlagView
|
||||
from mirage.commands.spec.usage import extra_operand_error
|
||||
from mirage.io.types import ByteSource, IOResult
|
||||
from mirage.types import PathSpec
|
||||
from mirage.types import PathSpec, ReadStreamFn
|
||||
|
||||
|
||||
async def _xxd_dump_stream(source: AsyncIterator[bytes], cols: int, group: int,
|
||||
@@ -182,7 +182,12 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> XxdFlags:
|
||||
)
|
||||
|
||||
|
||||
async def xxd_generic(paths, texts, opts: CommandOpts, read_stream):
|
||||
async def xxd_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_stream: ReadStreamFn,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await xxd(paths,
|
||||
read_stream=read_stream,
|
||||
|
||||
@@ -285,8 +285,15 @@ def parse_flags(flags: Mapping[str, FlagValue]) -> ZipFlags:
|
||||
)
|
||||
|
||||
|
||||
async def zip_generic(paths, texts, opts: CommandOpts, read_bytes, write_bytes,
|
||||
stat, walk):
|
||||
async def zip_generic(
|
||||
paths: list[PathSpec],
|
||||
texts: list[str],
|
||||
opts: CommandOpts,
|
||||
read_bytes: Callable[..., Awaitable[bytes]],
|
||||
write_bytes: Callable[..., Awaitable[None]],
|
||||
stat: StatFn,
|
||||
walk: WalkFn,
|
||||
) -> tuple[ByteSource | None, IOResult]:
|
||||
parsed = parse_flags(opts.flags)
|
||||
return await zip_cmd(
|
||||
paths,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any
|
||||
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
|
||||
PATH_TREE_ID = "__path_tree__"
|
||||
PAGE_CHUNK_BATCH_SIZE = 100
|
||||
|
||||
|
||||
async def fetch_path_tree(accessor) -> str:
|
||||
async def fetch_path_tree(accessor: ChromaAccessor, ) -> str:
|
||||
collection = await accessor.get_collection()
|
||||
result = await collection.get(ids=[PATH_TREE_ID])
|
||||
documents = result.get("documents") or []
|
||||
@@ -19,18 +21,27 @@ async def fetch_path_tree(accessor) -> str:
|
||||
return str(value)
|
||||
|
||||
|
||||
async def fetch_page_chunks(accessor, slug: str) -> str:
|
||||
async def fetch_page_chunks(
|
||||
accessor: ChromaAccessor,
|
||||
slug: str,
|
||||
) -> str:
|
||||
chunks = await page_chunks(accessor, slug)
|
||||
return "\n".join(chunk["document"] for chunk in chunks)
|
||||
|
||||
|
||||
async def iter_page_chunks(accessor, slug: str) -> AsyncIterator[str]:
|
||||
async def iter_page_chunks(
|
||||
accessor: ChromaAccessor,
|
||||
slug: str,
|
||||
) -> AsyncIterator[str]:
|
||||
chunks = await page_chunks(accessor, slug)
|
||||
for chunk in chunks:
|
||||
yield chunk["document"]
|
||||
|
||||
|
||||
async def page_chunks(accessor, slug: str) -> list[dict[str, Any]]:
|
||||
async def page_chunks(
|
||||
accessor: ChromaAccessor,
|
||||
slug: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
collection = await accessor.get_collection()
|
||||
chunks: list[dict[str, Any]] = []
|
||||
offset = 0
|
||||
@@ -57,7 +68,7 @@ async def page_chunks(accessor, slug: str) -> list[dict[str, Any]]:
|
||||
|
||||
|
||||
async def pages_chunks(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
slugs: list[str],
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Fetch every chunk of several pages in one scan.
|
||||
@@ -108,7 +119,7 @@ async def pages_chunks(
|
||||
|
||||
|
||||
async def query_contains(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
pattern: str,
|
||||
candidate_slugs: list[str],
|
||||
*,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.commands.builtin.grep_helper import compile_pattern, grep_lines
|
||||
from mirage.commands.builtin.utils.lines import split_lines
|
||||
@@ -9,7 +10,7 @@ from mirage.utils.key_prefix import mount_key, mount_prefix_of, rekey
|
||||
|
||||
|
||||
async def grep_bytes(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
paths: list[PathSpec],
|
||||
pattern: str,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
@@ -80,7 +81,7 @@ async def grep_bytes(
|
||||
|
||||
|
||||
async def coarse_filter_slugs(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
pattern: str,
|
||||
targets: dict[str, str],
|
||||
*,
|
||||
@@ -97,9 +98,11 @@ async def coarse_filter_slugs(
|
||||
regex=not fixed_string)
|
||||
|
||||
|
||||
async def target_slugs(accessor,
|
||||
paths: list[PathSpec],
|
||||
index: IndexCacheStore = NULL_INDEX) -> dict[str, str]:
|
||||
async def target_slugs(
|
||||
accessor: ChromaAccessor,
|
||||
paths: list[PathSpec],
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> dict[str, str]:
|
||||
targets: dict[str, str] = {}
|
||||
for path in paths:
|
||||
resolved = await resolve_path(accessor, path, index)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import errno
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.chroma._client import iter_page_chunks, page_chunks
|
||||
from mirage.core.chroma.path import resolve_path
|
||||
@@ -8,9 +9,11 @@ from mirage.core.chroma.render import render_page
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
async def read_bytes(accessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX) -> bytes:
|
||||
async def read_bytes(
|
||||
accessor: ChromaAccessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> bytes:
|
||||
resolved = await resolve_path(accessor, path, index)
|
||||
if resolved.is_dir:
|
||||
raise IsADirectoryError(errno.EISDIR, "Is a directory", path.virtual)
|
||||
@@ -19,9 +22,10 @@ async def read_bytes(accessor,
|
||||
|
||||
|
||||
async def read_stream(
|
||||
accessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX) -> AsyncIterator[bytes]:
|
||||
accessor: ChromaAccessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> AsyncIterator[bytes]:
|
||||
resolved = await resolve_path(accessor, path, index)
|
||||
if resolved.is_dir:
|
||||
raise IsADirectoryError(errno.EISDIR, "Is a directory", path.virtual)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.chroma.path import resolve_path
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.errors import enoent, enotdir
|
||||
|
||||
|
||||
async def readdir(accessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX) -> list[str]:
|
||||
async def readdir(
|
||||
accessor: ChromaAccessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> list[str]:
|
||||
resolved = await resolve_path(accessor, path, index)
|
||||
if not resolved.is_dir:
|
||||
raise enotdir(path)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore, IndexEntry
|
||||
from mirage.core.chroma._client import pages_chunks
|
||||
from mirage.core.chroma.render import render_page
|
||||
|
||||
|
||||
async def ensure_dir_sizes(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
directory: str,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> None:
|
||||
|
||||
@@ -3,14 +3,17 @@ import gzip
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore, IndexEntry
|
||||
from mirage.core.chroma._client import fetch_path_tree
|
||||
from mirage.utils.path import gnu_basename, parent
|
||||
|
||||
|
||||
async def ensure_tree(accessor,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
prefix: str = "") -> None:
|
||||
async def ensure_tree(
|
||||
accessor: ChromaAccessor,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
prefix: str = '',
|
||||
) -> None:
|
||||
root_key = mount_root(prefix)
|
||||
listing = await index.list_dir(root_key)
|
||||
if listing.entries is not None:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.chroma.path import resolve_path
|
||||
from mirage.core.chroma.readdir import readdir
|
||||
@@ -6,7 +7,7 @@ from mirage.utils.key_prefix import rekey
|
||||
|
||||
|
||||
async def walk(
|
||||
accessor,
|
||||
accessor: ChromaAccessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
*,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from mirage.accessor.dify import DifyAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.dify.path import resolve_path
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.errors import enoent, enotdir
|
||||
|
||||
|
||||
async def readdir(accessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX) -> list[str]:
|
||||
async def readdir(
|
||||
accessor: DifyAccessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> list[str]:
|
||||
resolved = await resolve_path(accessor, path, index)
|
||||
if not resolved.is_dir:
|
||||
raise enotdir(path)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.dify import DifyAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.dify.path import resolve_path
|
||||
from mirage.core.dify.readdir import readdir
|
||||
@@ -6,7 +7,7 @@ from mirage.utils.key_prefix import rekey
|
||||
|
||||
|
||||
async def walk(
|
||||
accessor,
|
||||
accessor: DifyAccessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
*,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from mirage.accessor.github import GitHubAccessor
|
||||
from mirage.cache.index import (NULL_INDEX, IndexCacheStore, IndexEntry,
|
||||
LookupStatus)
|
||||
from mirage.core.github.tree import (ensure_live_index, fetch_dir_tree,
|
||||
@@ -25,9 +26,11 @@ from mirage.utils.key_prefix import mount_prefix_of
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def readdir(accessor,
|
||||
path_spec: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX) -> list[str]:
|
||||
async def readdir(
|
||||
accessor: GitHubAccessor,
|
||||
path_spec: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> list[str]:
|
||||
virtual = path_spec.virtual
|
||||
prefix = mount_prefix_of(path_spec.virtual, path_spec.resource_path)
|
||||
path = (path_spec.dir if path_spec.pattern else path_spec).mount_path
|
||||
@@ -53,7 +56,7 @@ async def readdir(accessor,
|
||||
|
||||
|
||||
async def _fallback_readdir(
|
||||
accessor,
|
||||
accessor: GitHubAccessor,
|
||||
virtual_key: str,
|
||||
index: IndexCacheStore,
|
||||
virtual: str,
|
||||
@@ -94,10 +97,12 @@ async def _fallback_readdir(
|
||||
return sorted(child_keys)
|
||||
|
||||
|
||||
async def _resolve_dir_sha(accessor,
|
||||
virtual_key: str,
|
||||
index: IndexCacheStore,
|
||||
prefix: str = "") -> str | None:
|
||||
async def _resolve_dir_sha(
|
||||
accessor: GitHubAccessor,
|
||||
virtual_key: str,
|
||||
index: IndexCacheStore,
|
||||
prefix: str = '',
|
||||
) -> str | None:
|
||||
"""Get the tree SHA for a directory path.
|
||||
|
||||
Walks from root if needed, fetching per-directory trees.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mirage.accessor.github import GitHubAccessor
|
||||
from mirage.core.github._client import (GitHubApiError, github_get,
|
||||
github_request)
|
||||
from mirage.core.github.config import GhConfig, GitHubConfig
|
||||
@@ -37,7 +38,7 @@ async def fetch_default_branch(config: GitHubConfig, owner: str,
|
||||
return data["default_branch"]
|
||||
|
||||
|
||||
async def ensure_default_branch(accessor) -> str:
|
||||
async def ensure_default_branch(accessor: GitHubAccessor, ) -> str:
|
||||
"""Fetch the repo's default branch once, on the first read needing it.
|
||||
|
||||
The mount names a repository without contacting it, so this is the
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from mirage.accessor.github import GitHubAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.github.readdir import readdir as _readdir
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
@@ -24,9 +25,11 @@ from mirage.utils.key_prefix import mount_key, mount_prefix_of
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def stat(accessor,
|
||||
path_spec: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX) -> FileStat:
|
||||
async def stat(
|
||||
accessor: GitHubAccessor,
|
||||
path_spec: PathSpec,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
) -> FileStat:
|
||||
virtual = path_spec.virtual
|
||||
prefix = mount_prefix_of(path_spec.virtual, path_spec.resource_path)
|
||||
rel = path_spec.mount_path.strip("/")
|
||||
|
||||
@@ -17,6 +17,7 @@ from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from mirage.accessor.github import GitHubAccessor
|
||||
from mirage.cache.index import (NULL_INDEX, IndexCacheStore, IndexEntry,
|
||||
LookupStatus)
|
||||
from mirage.core.github._client import github_get
|
||||
@@ -156,7 +157,11 @@ def index_rows(
|
||||
return entries, children
|
||||
|
||||
|
||||
def seed_index(accessor, index: IndexCacheStore, prefix: str) -> None:
|
||||
def seed_index(
|
||||
accessor: GitHubAccessor,
|
||||
index: IndexCacheStore,
|
||||
prefix: str,
|
||||
) -> None:
|
||||
"""Write the accessor's tree into ``index`` under ``prefix``.
|
||||
|
||||
Args:
|
||||
@@ -169,7 +174,11 @@ def seed_index(accessor, index: IndexCacheStore, prefix: str) -> None:
|
||||
datetime.now(timezone.utc) + timedelta(days=365))
|
||||
|
||||
|
||||
async def refill_index(accessor, index: IndexCacheStore, prefix: str) -> bool:
|
||||
async def refill_index(
|
||||
accessor: GitHubAccessor,
|
||||
index: IndexCacheStore,
|
||||
prefix: str,
|
||||
) -> bool:
|
||||
"""Refetch the recursive tree and re-seed the index from it.
|
||||
|
||||
The mount fetches the whole tree once and seeds the index with it, so
|
||||
@@ -201,8 +210,11 @@ async def refill_index(accessor, index: IndexCacheStore, prefix: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def ensure_live_index(accessor, index: IndexCacheStore,
|
||||
prefix: str) -> bool:
|
||||
async def ensure_live_index(
|
||||
accessor: GitHubAccessor,
|
||||
index: IndexCacheStore,
|
||||
prefix: str,
|
||||
) -> bool:
|
||||
"""Refetch when the index holds no listing at all.
|
||||
|
||||
Every reader here treats a missing listing as a real absence, which
|
||||
@@ -250,9 +262,11 @@ async def ensure_live_index(accessor, index: IndexCacheStore,
|
||||
return await refill_index(accessor, index, prefix)
|
||||
|
||||
|
||||
async def ensure_tree(accessor,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
prefix: str = "") -> None:
|
||||
async def ensure_tree(
|
||||
accessor: GitHubAccessor,
|
||||
index: IndexCacheStore = NULL_INDEX,
|
||||
prefix: str = '',
|
||||
) -> None:
|
||||
"""Fetch the recursive tree if this mount has not got one yet.
|
||||
|
||||
The mount is constructed without touching the network, so readers
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.core.chroma.grep import grep_bytes
|
||||
from mirage.ops.registry import op
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
@op("grep", resource="chroma")
|
||||
async def grep(accessor, paths: list[PathSpec], pattern: str, *, index,
|
||||
**kwargs) -> bytes:
|
||||
async def grep(
|
||||
accessor: ChromaAccessor,
|
||||
paths: list[PathSpec],
|
||||
pattern: str,
|
||||
*,
|
||||
index,
|
||||
**kwargs,
|
||||
) -> bytes:
|
||||
output, _reads = await grep_bytes(accessor, paths, pattern, index)
|
||||
return output
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.chroma import ChromaAccessor
|
||||
from mirage.core.chroma import search as search_core
|
||||
from mirage.ops.registry import op
|
||||
from mirage.types import PathSpec
|
||||
@@ -5,8 +6,14 @@ from mirage.utils.key_prefix import mount_prefix_of
|
||||
|
||||
|
||||
@op("search", resource="chroma")
|
||||
async def search(accessor, paths: list[PathSpec], query: str, *, index,
|
||||
**kwargs) -> bytes:
|
||||
async def search(
|
||||
accessor: ChromaAccessor,
|
||||
paths: list[PathSpec],
|
||||
query: str,
|
||||
*,
|
||||
index,
|
||||
**kwargs,
|
||||
) -> bytes:
|
||||
explicit_prefix = kwargs.pop("mount_prefix", "")
|
||||
mount_prefix = mount_prefix_of(
|
||||
paths[0].virtual, paths[0].resource_path) if paths else explicit_prefix
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
from mirage.accessor.dify import DifyAccessor
|
||||
from mirage.core.dify.grep import grep_bytes
|
||||
from mirage.ops.registry import op
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
@op("grep", resource="dify")
|
||||
async def grep(accessor, paths: list[PathSpec], pattern: str, *, index,
|
||||
**kwargs) -> bytes:
|
||||
async def grep(
|
||||
accessor: DifyAccessor,
|
||||
paths: list[PathSpec],
|
||||
pattern: str,
|
||||
*,
|
||||
index,
|
||||
**kwargs,
|
||||
) -> bytes:
|
||||
return await grep_bytes(accessor, paths, pattern, index)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from mirage.accessor.dify import DifyAccessor
|
||||
from mirage.core.dify import search as search_core
|
||||
from mirage.ops.registry import op
|
||||
from mirage.types import PathSpec
|
||||
@@ -5,8 +6,14 @@ from mirage.utils.key_prefix import mount_prefix_of
|
||||
|
||||
|
||||
@op("search", resource="dify")
|
||||
async def search(accessor, paths: list[PathSpec], query: str, *, index,
|
||||
**kwargs) -> bytes:
|
||||
async def search(
|
||||
accessor: DifyAccessor,
|
||||
paths: list[PathSpec],
|
||||
query: str,
|
||||
*,
|
||||
index,
|
||||
**kwargs,
|
||||
) -> bytes:
|
||||
explicit_prefix = kwargs.pop("mount_prefix", "")
|
||||
mount_prefix = mount_prefix_of(
|
||||
paths[0].virtual, paths[0].resource_path) if paths else explicit_prefix
|
||||
|
||||
@@ -56,7 +56,11 @@ class BoxResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -9,7 +9,7 @@ from mirage.ops.chroma import OPS as CHROMA_VFS_OPS
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.chroma.config import ChromaConfig
|
||||
from mirage.resource.chroma.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -45,7 +45,11 @@ class ChromaResource(BaseResource):
|
||||
for fn in CHROMA_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -84,7 +84,11 @@ class DatabricksVolumeResource(BaseResource):
|
||||
for op in DATABRICKS_VOLUME_OPS:
|
||||
self.register_op(op)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -9,7 +9,7 @@ from mirage.ops.dify import OPS as DIFY_VFS_OPS
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.dify.config import DifyConfig
|
||||
from mirage.resource.dify.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -41,7 +41,11 @@ class DifyResource(BaseResource):
|
||||
for fn in DIFY_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.discord.config import DiscordConfig
|
||||
from mirage.core.discord.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.discord.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -49,7 +49,11 @@ class DiscordResource(BaseResource):
|
||||
for fn in DISCORD_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -96,7 +96,11 @@ class DiskResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -29,7 +29,7 @@ from mirage.core.dropbox.write import write_bytes
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.dropbox.config import DropboxConfig
|
||||
from mirage.resource.dropbox.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
from mirage.watch.base import DeltaHook
|
||||
|
||||
@@ -77,7 +77,11 @@ class DropboxResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.email.config import EmailConfig
|
||||
from mirage.core.email.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.email.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -53,7 +53,11 @@ class EmailResource(BaseResource):
|
||||
for fn in OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -22,7 +22,7 @@ from mirage.ops.gcal import OPS as GCAL_VFS_OPS
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.gcal.config import GCalConfig
|
||||
from mirage.resource.gcal.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -50,7 +50,11 @@ class GCalResource(BaseResource):
|
||||
for fn in GCAL_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -20,7 +20,7 @@ from mirage.core.google._client import TokenManager
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.gdocs.config import GDocsConfig
|
||||
from mirage.resource.gdocs.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -51,7 +51,11 @@ class GDocsResource(BaseResource):
|
||||
for fn in GDOCS_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -21,7 +21,7 @@ from mirage.core.google._client import TokenManager
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.gdrive.config import GoogleDriveConfig
|
||||
from mirage.resource.gdrive.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
from mirage.watch.base import DeltaHook
|
||||
|
||||
@@ -56,7 +56,11 @@ class GoogleDriveResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -21,7 +21,7 @@ from mirage.core.github.tree_entry import TreeEntry
|
||||
from mirage.core.github.watch import build_delta_hook
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.github.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
from mirage.watch.base import DeltaHook
|
||||
|
||||
@@ -119,7 +119,11 @@ class GitHubResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, self._index)
|
||||
|
||||
@property
|
||||
|
||||
@@ -20,7 +20,7 @@ from mirage.core.google._client import TokenManager
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.gmail.config import GmailConfig
|
||||
from mirage.resource.gmail.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -55,7 +55,11 @@ class GmailResource(BaseResource):
|
||||
for fn in GMAIL_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -91,7 +91,11 @@ class GridFSResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -20,7 +20,7 @@ from mirage.core.gsheets.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.gsheets.config import GSheetsConfig
|
||||
from mirage.resource.gsheets.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -51,7 +51,11 @@ class GSheetsResource(BaseResource):
|
||||
for fn in GSHEETS_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -22,7 +22,7 @@ from mirage.ops.gslides import OPS as GSLIDES_VFS_OPS
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.gslides.config import GSlidesConfig
|
||||
from mirage.resource.gslides.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -51,7 +51,11 @@ class GSlidesResource(BaseResource):
|
||||
for fn in GSLIDES_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -83,7 +83,11 @@ class HfBucketsResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -82,7 +82,11 @@ class HfDatasetsResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -82,7 +82,11 @@ class HfModelsResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -82,7 +82,11 @@ class HfSpacesResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -21,7 +21,7 @@ from mirage.ops.jaeger import OPS as JAEGER_VFS_OPS
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.jaeger.config import JaegerConfig
|
||||
from mirage.resource.jaeger.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -47,7 +47,11 @@ class JaegerResource(BaseResource):
|
||||
for op in JAEGER_VFS_OPS:
|
||||
self.register_op(op)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(
|
||||
self.accessor,
|
||||
paths,
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.lancedb.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.lancedb.config import LanceDBConfig
|
||||
from mirage.resource.lancedb.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -52,7 +52,11 @@ class LanceDBResource(BaseResource):
|
||||
for fn in LANCEDB_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.langfuse.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.langfuse.config import LangfuseConfig
|
||||
from mirage.resource.langfuse.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -44,7 +44,11 @@ class LangfuseResource(BaseResource):
|
||||
for fn in LANGFUSE_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(
|
||||
self.accessor,
|
||||
paths,
|
||||
|
||||
@@ -21,7 +21,7 @@ from mirage.core.linear.readdir import readdir
|
||||
from mirage.core.linear.stat import stat
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.linear.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -58,7 +58,11 @@ class LinearResource(BaseResource):
|
||||
for op in LINEAR_VFS_OPS:
|
||||
self.register_op(op)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.mongodb.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
from mirage.resource.mongodb.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -47,7 +47,11 @@ class MongoDBResource(BaseResource):
|
||||
for op in MONGODB_VFS_OPS:
|
||||
self.register_op(op)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -55,7 +55,11 @@ class NextcloudResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -21,7 +21,7 @@ from mirage.core.notion.readdir import readdir
|
||||
from mirage.core.notion.stat import stat
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.notion.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -54,7 +54,11 @@ class NotionResource(BaseResource):
|
||||
for op in NOTION_VFS_OPS:
|
||||
self.register_op(op)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -96,7 +96,11 @@ class OneDriveResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.postgres.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.postgres.config import PostgresConfig
|
||||
from mirage.resource.postgres.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -47,7 +47,11 @@ class PostgresResource(BaseResource):
|
||||
for op in POSTGRES_VFS_OPS:
|
||||
self.register_op(op)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.qdrant.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.qdrant.config import QdrantConfig
|
||||
from mirage.resource.qdrant.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -48,7 +48,11 @@ class QdrantResource(BaseResource):
|
||||
for fn in QDRANT_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -87,7 +87,11 @@ class RAMResource(BaseResource):
|
||||
for ro in RAM_OPS:
|
||||
self.register_op(ro)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -108,7 +108,11 @@ class RedisResource(BaseResource):
|
||||
base = f"{self.name}:{self.url}"
|
||||
return f"{base}/{prefix}" if prefix else base
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -102,7 +102,11 @@ class S3Resource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -82,7 +82,11 @@ class SharePointResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
if prefix:
|
||||
paths = [
|
||||
dataclasses.replace(p,
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.slack.config import SlackConfig
|
||||
from mirage.core.slack.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.slack.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -50,7 +50,11 @@ class SlackResource(BaseResource):
|
||||
for fn in SLACK_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -40,7 +40,7 @@ from mirage.core.ssh.write import write_bytes
|
||||
from mirage.ops.ssh import OPS as SSH_OPS
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.ssh.prompt import PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
from mirage.watch.base import DeltaHook
|
||||
|
||||
@@ -95,7 +95,11 @@ class SSHResource(BaseResource):
|
||||
def delta_hook(self) -> DeltaHook:
|
||||
return build_delta_hook(self.accessor)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.trello.readdir import readdir
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.trello.config import TrelloConfig
|
||||
from mirage.resource.trello.prompt import PROMPT, WRITE_PROMPT
|
||||
from mirage.types import ResourceName
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.glob_walk import make_resolve_glob
|
||||
|
||||
_resolve_glob = make_resolve_glob(readdir)
|
||||
@@ -45,7 +45,11 @@ class TrelloResource(BaseResource):
|
||||
for fn in TRELLO_VFS_OPS:
|
||||
self.register_op(fn)
|
||||
|
||||
async def resolve_glob(self, paths, prefix: str = ""):
|
||||
async def resolve_glob(
|
||||
self,
|
||||
paths: list[str | PathSpec],
|
||||
prefix: str = '',
|
||||
) -> list[PathSpec]:
|
||||
return await _resolve_glob(self.accessor, paths, index=self._index)
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from mirage.server.version.errors import NoSuchBranchError
|
||||
from mirage.server.version.state_tree import (CONTROL_PREFIX, META_PATH,
|
||||
@@ -23,6 +23,9 @@ from mirage.types import DriftPolicy, StateKey
|
||||
from mirage.workspace.snapshot import (apply_state_dict, install_fingerprints,
|
||||
to_state_dict)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
|
||||
async def snapshot_tree_from_state(store: VersionStore,
|
||||
state: dict[str, Any]) -> bytes:
|
||||
@@ -34,10 +37,12 @@ async def snapshot_tree_from_state(store: VersionStore,
|
||||
return await store.write_tree(tree_entries)
|
||||
|
||||
|
||||
async def commit(store: VersionStore,
|
||||
ws,
|
||||
branch: str = "main",
|
||||
message: str = "") -> bytes:
|
||||
async def commit(
|
||||
store: VersionStore,
|
||||
ws: "Workspace",
|
||||
branch: str = 'main',
|
||||
message: str = '',
|
||||
) -> bytes:
|
||||
return await commit_state(store, await to_state_dict(ws), branch, message)
|
||||
|
||||
|
||||
@@ -86,10 +91,12 @@ async def resolve_ref(store: VersionStore, ref) -> bytes:
|
||||
return ref
|
||||
|
||||
|
||||
async def checkout(store: VersionStore,
|
||||
ws,
|
||||
ref,
|
||||
drift_policy: DriftPolicy = DriftPolicy.STRICT) -> None:
|
||||
async def checkout(
|
||||
store: VersionStore,
|
||||
ws: "Workspace",
|
||||
ref,
|
||||
drift_policy: DriftPolicy = DriftPolicy.STRICT,
|
||||
) -> None:
|
||||
version = await resolve_ref(store, ref)
|
||||
entries, meta = await read_version(store, version)
|
||||
state = to_state(entries, meta)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from mirage.server.version.api import read_version, resolve_ref
|
||||
from mirage.server.version.state_tree import CATEGORIES, to_state
|
||||
@@ -22,6 +22,9 @@ from mirage.utils.path import norm
|
||||
from mirage.workspace.snapshot import (apply_state_dict, install_fingerprints,
|
||||
to_state_dict)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
|
||||
def _selected_file(path: str, wanted: list[str]) -> bool:
|
||||
p = norm(path)
|
||||
@@ -52,13 +55,14 @@ def _merge_mount_files(live_mount: dict[str, Any], target_mount: dict[str,
|
||||
|
||||
|
||||
async def restore(
|
||||
store: VersionStore,
|
||||
ws,
|
||||
ref,
|
||||
*,
|
||||
paths: list[str] | None = None,
|
||||
categories: list[str] | None = None,
|
||||
drift_policy: DriftPolicy = DriftPolicy.STRICT) -> dict[str, Any]:
|
||||
store: VersionStore,
|
||||
ws: "Workspace",
|
||||
ref,
|
||||
*,
|
||||
paths: list[str] | None = None,
|
||||
categories: list[str] | None = None,
|
||||
drift_policy: DriftPolicy = DriftPolicy.STRICT,
|
||||
) -> dict[str, Any]:
|
||||
"""Surgical restore: whole world, chosen categories, or chosen paths.
|
||||
|
||||
Scope rules: no arguments = the whole world (checkout semantics);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from mirage.config import load_config
|
||||
from mirage.utils.ids import new_workspace_id
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
WORKSPACE_CONFIG_CANDIDATES = (
|
||||
".mirage/workspace.yaml",
|
||||
".mirage/workspace.yml",
|
||||
"workspace.yaml",
|
||||
"workspace.yml",
|
||||
"mirage.yaml",
|
||||
"mirage.yml",
|
||||
)
|
||||
|
||||
DEFAULT_ENV_NAMES = ("MIRAGE_CONFIG", )
|
||||
|
||||
|
||||
def _require_config(path: Path) -> Path:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Mirage workspace config not found: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def resolve_workspace_config(
|
||||
config: str | Path | None = None,
|
||||
cwd: str | Path | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
env_names: tuple[str, ...] = DEFAULT_ENV_NAMES) -> Path:
|
||||
"""Find the workspace config a command should load.
|
||||
|
||||
An explicit path wins, then the first environment variable that is
|
||||
set, then the first candidate filename found walking up from cwd.
|
||||
|
||||
Args:
|
||||
config (str | Path | None): explicit path, relative to cwd.
|
||||
cwd (str | Path | None): directory to resolve from. Defaults to
|
||||
the process working directory.
|
||||
env (dict[str, str] | None): environment mapping to read.
|
||||
Defaults to ``os.environ``.
|
||||
env_names (tuple[str, ...]): variables to consult, in order.
|
||||
|
||||
Returns:
|
||||
Path: the resolved config path.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: a named path does not exist, or the walk
|
||||
reached the root without finding a candidate.
|
||||
"""
|
||||
base = Path(cwd).resolve() if cwd is not None else Path.cwd().resolve()
|
||||
use_env = env if env is not None else dict(os.environ)
|
||||
if config is not None:
|
||||
return _require_config((base / config).resolve())
|
||||
|
||||
for name in env_names:
|
||||
value = use_env.get(name)
|
||||
if value is not None:
|
||||
return _require_config((base / value).resolve())
|
||||
|
||||
for directory in (base, *base.parents):
|
||||
for candidate in WORKSPACE_CONFIG_CANDIDATES:
|
||||
path = directory / candidate
|
||||
if path.exists():
|
||||
return path
|
||||
raise FileNotFoundError(
|
||||
"No Mirage workspace config found. Pass a config path or set "
|
||||
f"{' or '.join(env_names)}.")
|
||||
|
||||
|
||||
async def build_workspace_from_config(config_path: str | Path) -> Workspace:
|
||||
"""Build a workspace from a config file, kernel mounts included.
|
||||
|
||||
Args:
|
||||
config_path (str | Path): path to the YAML config.
|
||||
|
||||
Returns:
|
||||
Workspace: the constructed workspace.
|
||||
"""
|
||||
config = load_config(config_path)
|
||||
kwargs = config.to_workspace_kwargs()
|
||||
kwargs["workspace_id"] = kwargs.get("workspace_id") or new_workspace_id()
|
||||
workspace = Workspace(**kwargs)
|
||||
try:
|
||||
for prefix, (backend, mountpoint) in config.kernel_mounts().items():
|
||||
workspace.add_fuse_mount(prefix, mountpoint, backend=backend)
|
||||
except Exception:
|
||||
await workspace.close()
|
||||
raise
|
||||
return workspace
|
||||
@@ -12,12 +12,22 @@
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from mirage.workspace.snapshot.manifest import split_manifest_and_blobs
|
||||
from mirage.workspace.snapshot.state import to_state_dict
|
||||
from mirage.workspace.snapshot.tar_io import write_tar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
async def snapshot(ws, target, *, compress: str | None = None) -> None:
|
||||
|
||||
async def snapshot(
|
||||
ws: "Workspace",
|
||||
target,
|
||||
*,
|
||||
compress: str | None = None,
|
||||
) -> None:
|
||||
"""Serialize a Workspace to a tar archive.
|
||||
|
||||
Fingerprints come from ``ws._ops.records`` (each read carries the
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from mirage.types import DriftPolicy, FingerprintKey
|
||||
from mirage.workspace.mount.mount import MountEntry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
TryMountFor = Callable[[str], MountEntry | None]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -113,7 +116,7 @@ class DriftQueue:
|
||||
raise result
|
||||
|
||||
|
||||
def capture_fingerprints(ws) -> list[dict[str, Any]]:
|
||||
def capture_fingerprints(ws: "Workspace", ) -> list[dict[str, Any]]:
|
||||
"""Walk session ops and emit one entry per distinct read on a
|
||||
``SUPPORTS_SNAPSHOT`` mount.
|
||||
|
||||
@@ -161,8 +164,11 @@ def capture_fingerprints(ws) -> list[dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
def install_fingerprints(ws, fingerprint_entries: list[dict[str, Any]],
|
||||
drift_policy: DriftPolicy) -> None:
|
||||
def install_fingerprints(
|
||||
ws: "Workspace",
|
||||
fingerprint_entries: list[dict[str, Any]],
|
||||
drift_policy: DriftPolicy,
|
||||
) -> None:
|
||||
"""Install snapshot fingerprints/revisions onto a reconstructed ws.
|
||||
|
||||
Revisions pin replay reads to exact backend versions; bare
|
||||
@@ -193,7 +199,7 @@ def install_fingerprints(ws, fingerprint_entries: list[dict[str, Any]],
|
||||
ws._drift.queue(path, fingerprint)
|
||||
|
||||
|
||||
def live_only_mount_prefixes(ws) -> list[str]:
|
||||
def live_only_mount_prefixes(ws: "Workspace", ) -> list[str]:
|
||||
"""Return mount prefixes whose resource opts out of snapshot replay.
|
||||
|
||||
These mounts will serve current state at load time with no drift
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from mirage.commands.builtin.utils.limit import (CommandTimeoutError,
|
||||
run_with_timeout)
|
||||
@@ -37,6 +37,9 @@ from mirage.workspace.workspace.failure import failure_result
|
||||
from mirage.workspace.workspace.line import run_whole_line
|
||||
from mirage.workspace.workspace.utils import command_name, fork_for_call
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -54,9 +57,13 @@ async def plan_eval_stub(cmd: str, **opts: Any) -> IOResult:
|
||||
return IOResult()
|
||||
|
||||
|
||||
async def recurse(ws, cancel: asyncio.Event | None,
|
||||
routing_decision: PolicyDecision | None, cmd: str,
|
||||
**opts: Any) -> Any:
|
||||
async def recurse(
|
||||
ws: "Workspace",
|
||||
cancel: asyncio.Event | None,
|
||||
routing_decision: PolicyDecision | None,
|
||||
cmd: str,
|
||||
**opts: Any,
|
||||
) -> Any:
|
||||
"""The executor's internal eval ($(), source, eval, xargs, ...).
|
||||
|
||||
Never a typed line, so it must not record a history entry or open
|
||||
@@ -78,7 +85,10 @@ async def recurse(ws, cancel: asyncio.Event | None,
|
||||
**opts)
|
||||
|
||||
|
||||
def session_cwd(ws, session_id: str) -> str | None:
|
||||
def session_cwd(
|
||||
ws: "Workspace",
|
||||
session_id: str,
|
||||
) -> str | None:
|
||||
"""The session's cwd for history, None once the session is gone.
|
||||
|
||||
Args:
|
||||
@@ -104,7 +114,7 @@ def syntax_error_result(offending: str) -> IOResult:
|
||||
|
||||
|
||||
async def execute_line(
|
||||
ws,
|
||||
ws: "Workspace",
|
||||
command: str,
|
||||
session_id: str | None,
|
||||
stdin: ByteSource | None,
|
||||
|
||||
@@ -15,14 +15,17 @@
|
||||
import asyncio
|
||||
import builtins
|
||||
import sys
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from mirage.ops.open import make_open
|
||||
from mirage.ops.os_patch import make_os_module
|
||||
from mirage.shell.job_table import cancel_job
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
def patch_process(ws) -> None:
|
||||
|
||||
def patch_process(ws: "Workspace", ) -> None:
|
||||
"""Point ``open`` and ``os`` at the workspace for a ``with`` block.
|
||||
|
||||
Args:
|
||||
@@ -34,17 +37,19 @@ def patch_process(ws) -> None:
|
||||
sys.modules["os"] = make_os_module(ws._ops)
|
||||
|
||||
|
||||
def unpatch_process(ws) -> None:
|
||||
def unpatch_process(ws: "Workspace", ) -> None:
|
||||
"""Restore the process-level ``open`` and ``os`` patched on entry.
|
||||
|
||||
Args:
|
||||
ws: the workspace leaving context-manager scope.
|
||||
"""
|
||||
builtins.open = ws._original_open
|
||||
sys.modules["os"] = ws._original_os
|
||||
if ws._original_open is not None:
|
||||
builtins.open = ws._original_open
|
||||
if ws._original_os is not None:
|
||||
sys.modules["os"] = ws._original_os
|
||||
|
||||
|
||||
def close_sync_parts(ws) -> None:
|
||||
def close_sync_parts(ws: "Workspace", ) -> None:
|
||||
"""Tear down everything that needs no event loop (idempotent).
|
||||
|
||||
Kernel mounts, running jobs, and in-flight cache drains; the
|
||||
@@ -68,7 +73,7 @@ def close_sync_parts(ws) -> None:
|
||||
ws._cache._drain_tasks.clear()
|
||||
|
||||
|
||||
async def close_async(ws) -> None:
|
||||
async def close_async(ws: "Workspace", ) -> None:
|
||||
"""Release everything the workspace owns, exactly once.
|
||||
|
||||
Order matters: the watch runtime goes first (it reads mounts), then
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Iterable, Mapping, Sequence
|
||||
from types import TracebackType
|
||||
from collections.abc import (AsyncIterator, Callable, Iterable, Mapping,
|
||||
Sequence)
|
||||
from types import ModuleType, TracebackType
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
from mirage.bridge.sync import run_async_from_sync
|
||||
@@ -182,6 +183,12 @@ class Workspace:
|
||||
links=self._namespace,
|
||||
dispatch=self._dispatcher.dispatch)
|
||||
self._kernel_mounts = KernelMounts(self._ops, self._session_mgr)
|
||||
# Held only while the workspace is a context manager; set by
|
||||
# lifecycle.patch_process. Declared here because the pair was
|
||||
# invented by assignment, so an unpatch without a patch raised
|
||||
# AttributeError instead of restoring nothing.
|
||||
self._original_open: Callable[..., Any] | None = None
|
||||
self._original_os: ModuleType | None = None
|
||||
|
||||
self._runtimes, self._policy_router = wire_runtime_world(
|
||||
self._registry, self.dispatch,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user