@struktoai/mirage-dsh 0.0.1 → 0.0.7-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ <h1 align="center">
2
+ <a href="https://www.strukto.ai/mirage">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/mirage-header-dark.svg">
5
+ <img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/mirage-header-light.svg" width="100%" alt="Mirage: A Virtual Terminal for AI Agents" />
6
+ </picture>
7
+ </a>
8
+ </h1>
9
+
10
+ <p align="center">
11
+ <a href="https://docs.mirage.strukto.ai" alt="Documentation">
12
+ <img src="https://img.shields.io/badge/mirage-docs-0C0C0C?labelColor=F0ECE2" /></a>
13
+ <a href="https://github.com/strukto-ai/mirage/releases" alt="Status">
14
+ <img src="https://img.shields.io/badge/status-preview-0C0C0C?labelColor=F0ECE2" alt="Status: preview" /></a>
15
+ <a href="https://www.strukto.ai" alt="Website">
16
+ <img src="https://img.shields.io/badge/made by-strukto.ai-0C0C0C?labelColor=F0ECE2" /></a>
17
+ <a href="https://github.com/strukto-ai/mirage/blob/main/LICENSE" alt="License">
18
+ <img src="https://img.shields.io/badge/license-Apache--2.0-0C0C0C?labelColor=F0ECE2" /></a>
19
+ <a href="https://discord.gg/u8BPQ65KsS" alt="Discord">
20
+ <img src="https://img.shields.io/badge/discord-join-0C0C0C?labelColor=F0ECE2&logo=discord&logoColor=0C0C0C" /></a>
21
+ <br/>
22
+ <a href="https://docs.mirage.strukto.ai/python/quickstart" alt="Python docs">
23
+ <img src="https://img.shields.io/badge/python-docs-0C0C0C?labelColor=F0ECE2&logo=python&logoColor=0C0C0C" alt="Python docs"></a>
24
+ <a href="https://pypi.org/project/mirage-ai/" alt="PyPI Version">
25
+ <img src="https://img.shields.io/pypi/v/mirage-ai.svg?color=0C0C0C&labelColor=F0ECE2"/></a>
26
+ <br/>
27
+ <a href="https://docs.mirage.strukto.ai/typescript/quickstart" alt="TypeScript docs">
28
+ <img src="https://img.shields.io/badge/typescript-docs-0C0C0C?labelColor=F0ECE2&logo=typescript&logoColor=0C0C0C" alt="TypeScript docs"></a>
29
+ <a href="https://www.npmjs.com/package/@struktoai/mirage-node" alt="NPM Version">
30
+ <img src="https://img.shields.io/npm/v/@struktoai/mirage-node.svg?color=0C0C0C&labelColor=F0ECE2"/></a>
31
+ </p>
32
+
33
+ <p align="center">
34
+ <a href="https://github.com/strukto-ai/mirage/blob/main/README.md"><img alt="README in English" src="https://img.shields.io/badge/English-F0ECE2"></a>
35
+ <a href="https://github.com/strukto-ai/mirage/blob/main/readme/README.zh-CN.md"><img alt="简体中文 README" src="https://img.shields.io/badge/简体中文-F0ECE2"></a>
36
+ <a href="https://github.com/strukto-ai/mirage/blob/main/readme/README.zh-TW.md"><img alt="繁體中文 README" src="https://img.shields.io/badge/繁體中文-F0ECE2"></a>
37
+ <a href="https://github.com/strukto-ai/mirage/blob/main/readme/README.fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-F0ECE2"></a>
38
+ <a href="https://github.com/strukto-ai/mirage/blob/main/readme/README.de.md"><img alt="README auf Deutsch" src="https://img.shields.io/badge/Deutsch-F0ECE2"></a>
39
+ <a href="https://github.com/strukto-ai/mirage/blob/main/readme/README.vi.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-F0ECE2"></a>
40
+ <a href="https://github.com/strukto-ai/mirage/blob/main/readme/README.ko.md"><img alt="README 한국어" src="https://img.shields.io/badge/%ED%95%9C%EA%B5%AD%EC%96%B4-F0ECE2"></a>
41
+ </p>
42
+
43
+ Mirage is **a Virtual Terminal for AI Agents**. The virtual filesystem delivers broad data context, virtualized CLIs give an agent more flexibility on tool use, dynamic runtimes save underlying infrastructure cost and are more token efficient, and fine-grained control over an agent's actions and even over what it can see gives the best security. Together these parts form one virtualized terminal, giving the best agent performance, cost efficiency and security.
44
+
45
+ <p align="center">
46
+ <picture>
47
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/mirage-arch-dark.svg">
48
+ <img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/mirage-arch-light.svg" alt="Mirage architecture: agents and harness reach profiles and the Mirage shell, which resolve Unix-like commands, virtual CLIs and programming languages onto runtimes and the virtual filesystem, with authentication, the policy engine and notifications alongside" width="100%">
49
+ </picture>
50
+ </p>
51
+
52
+ Here is an example of launching Mirage inside an application:
53
+
54
+ ```python
55
+ ws = Workspace(
56
+ {
57
+ "/tmp": (RAMResource(), MountMode.EXEC),
58
+ "/redis": (RedisResource(url=redis_url), MountMode.WRITE),
59
+ "/slack": (SlackResource(SlackConfig(token=slack_bot_token)), MountMode.EXEC),
60
+ },
61
+ # monty captures python, so scripts run sandboxed inside the workspace
62
+ runtimes=[MontyRuntime(captures=["python", "python3"]), "vfs"],
63
+ )
64
+
65
+ # one grep sweeps every source
66
+ await ws.execute("grep -rln session /redis /tmp")
67
+
68
+ # run a script that lives in Slack, file the report into Redis
69
+ await ws.execute("python3 /slack/channels/general_.../files/example__F....py > /redis/report.txt")
70
+
71
+ # install a typed CLI under a head word: dispatched by name, not by path,
72
+ # and discoverable through `man`, `type` and `which` like any other program
73
+ ws.register_cli("slack", SLACK, {"token": slack_bot_token})
74
+ await ws.execute('slack send-message --channel general --text "report is up"')
75
+ ```
76
+
77
+ ## About
78
+
79
+ - **Unified virtual terminal interface, not N SDKs and M MCPs.** Every backend speaks the same filesystem semantics, so pipelines compose across services.
80
+ - **A virtual filesystem over every source.** S3, Google Drive, Slack, Gmail, Redis and the rest mount side by side under one root, so an agent reaches all of them through a unified interface with the unix tools it already knows, like `ls`, `grep`, `find` and `jq`.
81
+ - **Virtual command line tools (CLIs).** `git`, `slack` and `ntn` are answered by Mirage itself, so an agent drives the service with nothing installed, across different runtimes and machines, and one tool can be virtualized into two or more, each under its own name with its own credentials.
82
+ - **Routed, dynamic runtimes.** Python, JavaScript and any other command can be sent to a configured runtime, in process, sandboxed or remote, which decouples computation from storage and lets either change without touching the other.
83
+ - **The virtualized Mirage shell.** It binds the filesystem, the CLIs and the runtimes into one command line, so pipes, redirection, variables, jobs and history work across all three.
84
+ - **Profiles designed for agents.** `allow`, `ask` and `deny` govern commands and CLIs, while `hide` and `show` govern files and folders, so a hidden path is not merely unreadable but absent from the filesystem the agent sees.
85
+ - **A scriptable policy engine.** A policy script can prohibit any dangerous action before it runs, and the same stack gates every VFS op and session write, so neither a file nor an environment variable leaks.
86
+ - **Notifications wired into the VFS and agents.** External changes become an event stream on the mount, so a new Slack reply surfaces as a change to the chat file in the virtual filesystem, and the agent reacts to it instead of rescanning the tree.
87
+
88
+ ## Virtual Filesystem
89
+
90
+ Everything [Mirage](https://www.strukto.ai/mirage) "mounts" as one unified virtual filesystem for AI agents. Each
91
+ service sits side-by-side under a single root and answers the same POSIX semantics.
92
+
93
+ | | Resources |
94
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | **Object Storage** | <a href="https://aws.amazon.com/s3/"><img src="https://api.iconify.design/logos/aws-s3.svg" width="20" height="20" alt="Amazon S3" title="Amazon S3" /></a>  <a href="https://developers.cloudflare.com/r2/"><img src="https://cdn.simpleicons.org/cloudflare" width="20" height="20" alt="Cloudflare R2" title="Cloudflare R2" /></a>  <a href="https://cloud.google.com/storage"><img src="https://api.iconify.design/logos/google-cloud.svg" width="25" height="20" alt="Google Cloud Storage" title="Google Cloud Storage" /></a>  <a href="https://www.oracle.com/cloud/storage/"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/oracle.svg" width="32" height="20" alt="Oracle OCI" title="Oracle OCI" /></a>  <a href="https://supabase.com/storage"><img src="https://api.iconify.design/logos/supabase-icon.svg" width="19" height="20" alt="Supabase Storage" title="Supabase Storage" /></a>  <a href="https://min.io"><img src="https://cdn.simpleicons.org/minio" width="20" height="20" alt="MinIO" title="MinIO" /></a>  <a href="https://ceph.io/en/discover/technology/"><img src="https://cdn.simpleicons.org/ceph" width="20" height="20" alt="Ceph" title="Ceph" /></a>  <a href="https://github.com/seaweedfs/seaweedfs"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/seaweedfs.svg" width="20" height="20" alt="SeaweedFS" title="SeaweedFS" /></a>  <a href="https://wasabi.com/cloud-object-storage"><img src="https://cdn.simpleicons.org/wasabi" width="20" height="20" alt="Wasabi" title="Wasabi" /></a>  <a href="https://www.backblaze.com/cloud-storage"><img src="https://cdn.simpleicons.org/backblaze" width="20" height="20" alt="Backblaze B2" title="Backblaze B2" /></a>  <a href="https://www.digitalocean.com/products/spaces"><img src="https://cdn.simpleicons.org/digitalocean" width="20" height="20" alt="DigitalOcean Spaces" title="DigitalOcean Spaces" /></a>  <a href="https://www.alibabacloud.com/product/oss"><img src="https://cdn.simpleicons.org/alibabacloud" width="20" height="20" alt="Alibaba Cloud OSS" title="Alibaba Cloud OSS" /></a>  <a href="https://www.tencentcloud.com/products/cos"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/tencent.png" width="20" height="20" alt="Tencent Cloud COS" title="Tencent Cloud COS" /></a>  <a href="https://www.scaleway.com/en/object-storage/"><img src="https://cdn.simpleicons.org/scaleway" width="20" height="20" alt="Scaleway" title="Scaleway" /></a>  <a href="https://www.qingcloud.com/products/qingstor"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/qingstor.png" width="20" height="20" alt="QingStor" title="QingStor" /></a>  <a href="https://huggingface.co/docs/hub/storage-buckets"><img src="https://cdn.simpleicons.org/huggingface" width="20" height="20" alt="Hugging Face Buckets" title="Hugging Face Buckets" /></a>  <a href="https://www.mongodb.com/docs/manual/core/gridfs/"><img src="https://cdn.simpleicons.org/mongodb" width="20" height="20" alt="GridFS" title="GridFS" /></a> |
96
+ | **Files and Documents** | <a href="https://workspace.google.com/products/drive/"><img src="https://cdn.simpleicons.org/googledrive" width="20" height="20" alt="Google Drive" title="Google Drive" /></a>  <a href="https://docs.google.com"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google-docs.svg" width="14" height="20" alt="Google Docs" title="Google Docs" /></a>  <a href="https://sheets.google.com"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google-sheets.svg" width="15" height="20" alt="Google Sheets" title="Google Sheets" /></a>  <a href="https://slides.google.com"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google-slides.svg" width="15" height="20" alt="Google Slides" title="Google Slides" /></a>  <a href="https://www.microsoft.com/microsoft-365/onedrive/online-cloud-storage"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/microsoft-onedrive.svg" width="30" height="20" alt="OneDrive" title="OneDrive" /></a>  <a href="https://www.microsoft.com/microsoft-365/sharepoint/collaboration"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/microsoft-sharepoint.svg" width="20" height="20" alt="SharePoint" title="SharePoint" /></a>  <a href="https://www.box.com"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/box.svg" width="20" height="20" alt="Box" title="Box" /></a>  <a href="https://www.dropbox.com"><img src="https://cdn.simpleicons.org/dropbox" width="20" height="20" alt="Dropbox" title="Dropbox" /></a>  <a href="https://nextcloud.com/files/"><img src="https://cdn.simpleicons.org/nextcloud" width="20" height="20" alt="Nextcloud" title="Nextcloud" /></a> |
97
+ | **Messaging and Work** | <a href="https://slack.com"><img src="https://api.iconify.design/logos/slack-icon.svg" width="20" height="20" alt="Slack" title="Slack" /></a>  <a href="https://discord.com"><img src="https://cdn.simpleicons.org/discord" width="20" height="20" alt="Discord" title="Discord" /></a>  <a href="https://mail.google.com"><img src="https://cdn.simpleicons.org/gmail" width="20" height="20" alt="Gmail" title="Gmail" /></a>  <a href="https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol"><img src="https://api.iconify.design/lucide/mail.svg?color=%23888888" width="20" height="20" alt="IMAP / SMTP email" title="IMAP / SMTP email" /></a>  <a href="https://github.com"><img src="https://cdn.simpleicons.org/github/181717/e6edf3" width="20" height="20" alt="GitHub" title="GitHub" /></a>  <a href="https://linear.app"><img src="https://cdn.simpleicons.org/linear" width="20" height="20" alt="Linear" title="Linear" /></a>  <a href="https://www.notion.so"><img src="https://cdn.simpleicons.org/notion/000000/e6edf3" width="20" height="20" alt="Notion" title="Notion" /></a>  <a href="https://trello.com"><img src="https://cdn.simpleicons.org/trello" width="20" height="20" alt="Trello" title="Trello" /></a>  <a href="https://workspace.google.com/products/calendar/"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google-calendar.svg" width="20" height="20" alt="Google Calendar" title="Google Calendar" /></a> |
98
+ | **Databases and Data Platforms** | <a href="https://www.postgresql.org"><img src="https://cdn.simpleicons.org/postgresql" width="20" height="20" alt="PostgreSQL" title="PostgreSQL" /></a>  <a href="https://www.mongodb.com"><img src="https://cdn.simpleicons.org/mongodb" width="20" height="20" alt="MongoDB" title="MongoDB" /></a>  <a href="https://redis.io"><img src="https://cdn.simpleicons.org/redis" width="20" height="20" alt="Redis" title="Redis" /></a> <a href="https://upstash.com"><img src="https://cdn.simpleicons.org/upstash" width="20" height="20" alt="Upstash Redis" title="Upstash Redis" /></a>  <a href="https://lancedb.com"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/lancedb.png" width="20" height="20" alt="LanceDB" title="LanceDB" /></a>  <a href="https://qdrant.tech"><img src="https://cdn.simpleicons.org/qdrant" width="20" height="20" alt="Qdrant" title="Qdrant" /></a>  <a href="https://www.trychroma.com"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/chroma.svg" width="31" height="20" alt="Chroma" title="Chroma" /></a>  <a href="https://mem0.ai"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/mem0.svg" width="20" height="20" alt="Mem0" title="Mem0" /></a>  <a href="https://huggingface.co/datasets"><img src="https://cdn.simpleicons.org/huggingface" width="20" height="20" alt="Hugging Face Datasets" title="Hugging Face Datasets" /></a>  <a href="https://huggingface.co/models"><img src="https://cdn.simpleicons.org/huggingface" width="20" height="20" alt="Hugging Face Models" title="Hugging Face Models" /></a>  <a href="https://huggingface.co/spaces"><img src="https://cdn.simpleicons.org/huggingface" width="20" height="20" alt="Hugging Face Spaces" title="Hugging Face Spaces" /></a>  <a href="https://docs.databricks.com/aws/en/volumes/"><img src="https://cdn.simpleicons.org/databricks" width="20" height="20" alt="Databricks Volumes" title="Databricks Volumes" /></a>  <a href="https://dify.ai"><img src="https://cdn.simpleicons.org/dify" width="20" height="20" alt="Dify" title="Dify" /></a> |
99
+ | **Observability** | <a href="https://langfuse.com"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/langfuse-logo.svg" width="20" height="20" alt="Langfuse" title="Langfuse" /></a>  <a href="https://www.jaegertracing.io"><img src="https://cdn.simpleicons.org/jaeger" width="20" height="20" alt="Jaeger" title="Jaeger" /></a> |
100
+ | **Local and Remote** | <a href="https://en.wikipedia.org/wiki/Random-access_memory"><img src="https://api.iconify.design/lucide/memory-stick.svg?color=%23888888" width="20" height="20" alt="RAM" title="RAM" /></a>  <a href="https://en.wikipedia.org/wiki/File_system"><img src="https://api.iconify.design/lucide/hard-drive.svg?color=%23888888" width="20" height="20" alt="Disk" title="Disk" /></a>  <a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system"><img src="https://api.iconify.design/lucide/folder-lock.svg?color=%23888888" width="20" height="20" alt="OPFS" title="OPFS" /></a>  <a href="https://en.wikipedia.org/wiki/Secure_Shell"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/ssh-logo.svg" width="20" height="20" alt="SSH" title="SSH" /></a> |
101
+
102
+ Agents reach it through the Python and TypeScript SDKs, the `mirage` CLI, or a real
103
+ mountpoint over FUSE and FSKit, then work it with the unix tools they already know,
104
+ like `ls`, `grep`, `find` and `jq`.
105
+
106
+ ## Virtual Command Line Tool
107
+
108
+ These command line tools are virtualized: [Mirage](https://www.strukto.ai/mirage) answers `git`, `slack` or `ntn`
109
+ itself, so an agent drives the service without that program being installed on the
110
+ machine. Each one mimics the real tool, so an agent that knows the CLI needs nothing
111
+ new. Because they are virtual, the same tool can be installed more than once under
112
+ different names, each with its own credentials, so every agent gets exactly the
113
+ accounts it is given.
114
+
115
+ | | CLIs |
116
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | **Code** | <a href="https://git-scm.com"><img src="https://cdn.simpleicons.org/git" width="20" height="20" alt="git" title="git" /></a>  <a href="https://cli.github.com"><img src="https://cdn.simpleicons.org/github/181717/e6edf3" width="20" height="20" alt="gh" title="gh" /></a> |
118
+ | **Communication** | <a href="https://slack.com"><img src="https://api.iconify.design/logos/slack-icon.svg" width="20" height="20" alt="slack" title="slack" /></a>  <a href="https://discord.com"><img src="https://cdn.simpleicons.org/discord" width="20" height="20" alt="discord" title="discord" /></a>  <a href="https://github.com/pimalaya/himalaya"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/himalaya.png" width="20" height="20" alt="himalaya" title="himalaya" /></a> |
119
+ | **Work and Data** | <a href="https://linear.app"><img src="https://cdn.simpleicons.org/linear" width="20" height="20" alt="linear" title="linear" /></a>  <a href="https://www.notion.so"><img src="https://cdn.simpleicons.org/notion/000000/e6edf3" width="20" height="20" alt="ntn" title="ntn" /></a>  <a href="https://workspace.google.com"><img src="https://cdn.simpleicons.org/google" width="20" height="20" alt="gws" title="gws" /></a>  <a href="https://huggingface.co"><img src="https://cdn.simpleicons.org/huggingface" width="20" height="20" alt="hf" title="hf" /></a> |
120
+
121
+ ## Virtual Runtime
122
+
123
+ Runtimes are virtualized the same way, and not only for coding languages. Any
124
+ command on the line can be redirected to a configured runtime, so Python might
125
+ run in-process with [Monty](https://pydantic.dev/articles/pydantic-monty) while
126
+ another command, say `kubectl`, is sent to a remote machine over SSH. Which runtime
127
+ serves a given line can be decided by a
128
+ [scripted runtime router](https://docs.mirage.strukto.ai/home/route-policy).
129
+
130
+ | | Runtimes |
131
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
132
+ | **Python** | <a href="https://pydantic.dev/articles/pydantic-monty"><img src="https://cdn.simpleicons.org/pydantic" width="20" height="20" alt="Monty" title="Monty" /></a>  <a href="https://wasi.dev"><img src="https://cdn.simpleicons.org/webassembly" width="20" height="20" alt="WASI CPython on wasmtime" title="WASI CPython on wasmtime" /></a>  <a href="https://pyodide.org"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/pyodide.svg" width="56" height="20" alt="Pyodide" title="Pyodide" /></a>  <a href="https://pypi.org/project/sandlock/"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/sandlock-logo.svg" width="20" height="20" alt="sandlock" title="sandlock" /></a>  <a href="https://www.python.org"><img src="https://cdn.simpleicons.org/python" width="20" height="20" alt="Host CPython" title="Host CPython" /></a> |
133
+ | **JavaScript** | <a href="https://docs.mirage.strukto.ai/home/setup/runtime/javascript"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/quickjs.png" width="20" height="20" alt="QuickJS, serving node and js" title="QuickJS, serving node and js" /></a> <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript"><img src="https://cdn.simpleicons.org/javascript" width="20" height="20" alt="JavaScript" title="JavaScript" /></a> <a href="https://www.typescriptlang.org"><img src="https://cdn.simpleicons.org/typescript" width="20" height="20" alt="TypeScript" title="TypeScript" /></a> |
134
+ | **Sandboxes** | <a href="https://www.docker.com"><img src="https://cdn.simpleicons.org/docker" width="20" height="20" alt="Docker" title="Docker" /></a>  <a href="https://e2b.dev"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/e2b-logo.svg" width="20" height="20" alt="E2B" title="E2B" /></a>  <a href="https://www.daytona.io"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/daytona-logo.svg" width="20" height="20" alt="Daytona" title="Daytona" /></a>  <a href="https://smolmachines.com"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/smolvm-logo.svg" width="20" height="20" alt="smolVM" title="smolVM" /></a>  <a href="https://en.wikipedia.org/wiki/Secure_Shell"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/docs/images/ssh-logo.svg" width="20" height="20" alt="SSH" title="SSH" /></a> |
135
+
136
+ ## Security
137
+
138
+ A profile decides what a session may run and what it may see. Commands are governed
139
+ by customizable `allow`, `ask` and `deny` rules, and paths by `hide` and `show`, so a
140
+ hidden file is not merely unreadable but absent from the filesystem the agent sees.
141
+ For anything those rules cannot express, a profile can name a policy script that runs
142
+ at the gate on every command and answers allow, deny or ask itself, though like every
143
+ rule it can only restrict and never grant. Separately, a host can register its own
144
+ policies on the [policy engine](https://docs.mirage.strukto.ai/home/policy-engine), an
145
+ ordered stack the workspace consults on every command, VFS op and session write. See
146
+ the [permissions docs](https://docs.mirage.strukto.ai/home/permissions).
147
+
148
+ ## Authentication
149
+
150
+ Credentials and authentication integrate with the stores secrets already live in,
151
+ including AWS Secrets Manager, 1Password, Auth0 and dotenv, so an environment variable
152
+ in Mirage can resolve straight to a credential held in one of them.
153
+
154
+ | | Sources |
155
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
156
+ | **Built in** | <a href="https://en.wikipedia.org/wiki/Environment_variable"><img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/icons/terminal.svg" width="20" height="20" alt="Environment" title="Environment" /></a> <a href="https://www.dotenv.org"><img src="https://cdn.simpleicons.org/dotenv" width="20" height="20" alt="dotenv" title="dotenv" /></a> <a href="https://aws.amazon.com/secrets-manager/"><img src="https://api.iconify.design/logos/aws-secrets-manager.svg" width="20" height="20" alt="AWS Secrets Manager" title="AWS Secrets Manager" /></a> |
157
+ | **Custom** | <a href="https://1password.com"><img src="https://cdn.simpleicons.org/1password" width="20" height="20" alt="1Password" title="1Password" /></a> <a href="https://auth0.com"><img src="https://cdn.simpleicons.org/auth0" width="20" height="20" alt="Auth0" title="Auth0" /></a> |
158
+
159
+ ## Installation
160
+
161
+ - **Python** ≥ 3.11 for the `mirage-ai` package and the `mirage` CLI
162
+ - **Node.js** ≥ 20 for the TypeScript SDK
163
+
164
+ ### Python
165
+
166
+ ```bash
167
+ uv add mirage-ai # installs the `mirage` library and the `mirage` CLI binary
168
+ ```
169
+
170
+ ### TypeScript
171
+
172
+ ```bash
173
+ npm install @struktoai/mirage-node # Node.js servers and CLIs
174
+ npm install @struktoai/mirage-browser # browser / edge runtimes
175
+ npm install @struktoai/mirage-agents # OpenAI / Vercel AI / LangChain / Mastra adapters
176
+ ```
177
+
178
+ Both runtime packages pull in `@struktoai/mirage-core` automatically.
179
+
180
+ ### CLI
181
+
182
+ ```bash
183
+ curl -fsSL https://strukto.ai/mirage/install.sh | sh
184
+ # or
185
+ npm install -g @struktoai/mirage-cli
186
+ # or
187
+ uvx mirage-ai
188
+ # or
189
+ npx @struktoai/mirage-cli
190
+ ```
191
+
192
+ ## Quickstart
193
+
194
+ ### Python
195
+
196
+ ```python
197
+ from mirage import Workspace
198
+ from mirage.resource.ram import RAMResource
199
+ from mirage.resource.s3 import S3Config, S3Resource
200
+
201
+ ws = Workspace({
202
+ "/data": RAMResource(),
203
+ "/s3": S3Resource(S3Config(bucket="my-bucket")),
204
+ })
205
+
206
+ await ws.execute("cp /s3/report.csv /data/report.csv")
207
+ await ws.execute("grep alert /s3/data/log.jsonl | wc -l")
208
+
209
+ await ws.snapshot("demo.tar")
210
+ ```
211
+
212
+ ### TypeScript
213
+
214
+ ```ts
215
+ import { Workspace, RAMResource, S3Resource } from '@struktoai/mirage-node'
216
+
217
+ const ws = new Workspace({
218
+ '/data': new RAMResource(),
219
+ '/s3': new S3Resource({ bucket: 'my-bucket' }),
220
+ })
221
+
222
+ await ws.execute('cp /s3/report.csv /data/report.csv')
223
+ await ws.execute('grep alert /s3/data/log.jsonl | wc -l')
224
+
225
+ await ws.snapshot('demo.tar')
226
+ ```
227
+
228
+ ### CLI
229
+
230
+ ```bash
231
+ mirage workspace create ws.yaml --id demo
232
+ mirage execute --workspace_id demo --command "cp /s3/report.csv /data/report.csv"
233
+ mirage provision --workspace_id demo --command "cat /s3/data/large.jsonl"
234
+ mirage workspace snapshot demo demo.tar
235
+ mirage workspace load demo.tar --id demo-restored
236
+ ```
237
+
238
+ ## Contributors
239
+
240
+ Thanks to everyone who has contributed to Mirage.
241
+
242
+ <a href="https://github.com/strukto-ai/mirage/graphs/contributors">
243
+ <img src="https://contrib.rocks/image?repo=strukto-ai/mirage" alt="Mirage contributors" />
244
+ </a>
package/cordis.patch.yml CHANGED
@@ -19,6 +19,55 @@
19
19
  # config: { token: !!js process.env.SLACK_BOT_TOKEN }
20
20
  # runtimes:
21
21
  # - { name: monty, captures: [python, python3] }
22
+ #
23
+ # A profile may also carry a permission document, which is what turns
24
+ # mirage's allow/ask/deny into a policy for this bundle. Roles go under
25
+ # `profiles`, and `profile` names the one a session gets by default:
26
+ #
27
+ # - id: mirage
28
+ # config:
29
+ # profiles:
30
+ # agent:
31
+ # commands:
32
+ # allow: [cat, ls, grep, rm]
33
+ # ask:
34
+ # - { commands: [rm], reason: deletes are reviewed }
35
+ # deny:
36
+ # - { commands: [git push], reason: no publishing }
37
+ # profile: agent
38
+ #
39
+ # An `ask` rule is put to the human through dsh's own approval channel
40
+ # (`ctx.approval`), so the prompt the agent triggers is the one dsh
41
+ # already shows for its escalations. It quotes the line word by word as
42
+ # a shell would read it back, so a name carrying a space or a newline
43
+ # cannot pass itself off in the prompt as two operands or two lines.
44
+ # `allowed-once` runs the line;
45
+ # a rejection, a dismissal and an unanswerable ask all refuse it. With
46
+ # no approval plugin composed the ask has nowhere to go, so it stays a
47
+ # pending record the host answers through `ctx.mirage.decisions`
48
+ # (`pending()`, then `answer(id, outcome, scope)`) — deliberately not a
49
+ # refusal, since rewriting an `ask` rule as a `deny` would change what
50
+ # the document says. A question never outlives the run that raised it:
51
+ # the run's abort signal goes to the channel with the request and bounds
52
+ # the ledger's wait, so a line still unanswered when its timeout expires
53
+ # comes back as the kill, and a yes arriving after that is dropped
54
+ # rather than banked for the next identical line.
55
+ #
56
+ # Two facts about how the mirage providers read the surrounding dsh
57
+ # composition, neither of which needs a row here:
58
+ #
59
+ # * The standing file policy is enforced, not merely reported. Both
60
+ # seams narrow every mount grant to read for a `read-only` call
61
+ # (leaving the null sink writable, as that mode's definition
62
+ # requires), so dsh-base's `DSH_PERMISSION_MODE=read-only` now
63
+ # actually confines a mirage world instead of being ignored. The
64
+ # policy's `workspaceRoot` is a directory on the host and is never
65
+ # consulted: the mounts and their modes are the boundary here.
66
+ #
67
+ # * A workdir dsh resolved on its own machine (an unspecified one
68
+ # comes from the calling session's cwd) names nothing in this world,
69
+ # so it is ignored in favour of the workspace default rather than
70
+ # run in a directory the agent cannot reach.
22
71
 
23
72
  # The host filesystem and bash providers step aside; mirage provides the
24
73
  # same ctx.fs and ctx.shell seams over the workspace.
@@ -48,16 +48,77 @@ function applyLiteralEdit(content, oldString, newString, replaceAll, displayPath
48
48
  }
49
49
  return replaceAll ? content.replaceAll(oldString, newString) : content.replace(oldString, newString);
50
50
  }
51
+ function charBoundary(bytes, from) {
52
+ let start = from;
53
+ while (start < bytes.byteLength && ((bytes[start] ?? 0) & 192) === 128) start++;
54
+ return start;
55
+ }
51
56
  function tailCap(text, maxBytes) {
52
57
  const bytes = new TextEncoder().encode(text);
53
58
  if (bytes.byteLength <= maxBytes) return { text, truncated: false };
54
- let start = bytes.byteLength - maxBytes;
55
- while (start < bytes.byteLength && ((bytes[start] ?? 0) & 192) === 128) start++;
59
+ const start = charBoundary(bytes, bytes.byteLength - maxBytes);
56
60
  return {
57
61
  text: new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(start)),
58
62
  truncated: true
59
63
  };
60
64
  }
65
+ var TailBuffer = class {
66
+ budget;
67
+ parts = [];
68
+ bytes = 0;
69
+ constructor(budget) {
70
+ this.budget = budget;
71
+ }
72
+ /**
73
+ * Append a chunk, dropping the oldest bytes that no longer fit.
74
+ *
75
+ * @param data the bytes to append.
76
+ * @returns true when bytes were dropped to make room.
77
+ */
78
+ append(data) {
79
+ this.parts.push(data);
80
+ this.bytes += data.byteLength;
81
+ if (this.bytes <= this.budget) return false;
82
+ let excess = this.bytes - this.budget;
83
+ while (excess > 0) {
84
+ const head = this.parts[0];
85
+ if (head === void 0) break;
86
+ if (head.byteLength <= excess) {
87
+ this.parts.shift();
88
+ excess -= head.byteLength;
89
+ this.bytes -= head.byteLength;
90
+ } else {
91
+ this.parts[0] = head.subarray(excess);
92
+ this.bytes -= excess;
93
+ excess = 0;
94
+ }
95
+ }
96
+ return true;
97
+ }
98
+ /**
99
+ * Drain everything held, decoded as text.
100
+ *
101
+ * The head may sit mid-character, since dropping is byte-exact, so it
102
+ * is re-aligned before decoding rather than rendered as a replacement
103
+ * mark.
104
+ *
105
+ * @returns the buffered text; the buffer is left empty.
106
+ */
107
+ take() {
108
+ if (this.parts.length === 0) return "";
109
+ const joined = new Uint8Array(this.bytes);
110
+ let at = 0;
111
+ for (const part of this.parts) {
112
+ joined.set(part, at);
113
+ at += part.byteLength;
114
+ }
115
+ this.parts = [];
116
+ this.bytes = 0;
117
+ return new TextDecoder("utf-8", { fatal: false }).decode(
118
+ joined.subarray(charBoundary(joined, 0))
119
+ );
120
+ }
121
+ };
61
122
 
62
123
  export {
63
124
  decodeStrictText,
@@ -65,6 +126,7 @@ export {
65
126
  detectsCrlf,
66
127
  restoreLineEndings,
67
128
  applyLiteralEdit,
68
- tailCap
129
+ tailCap,
130
+ TailBuffer
69
131
  };
70
- //# sourceMappingURL=chunk-CATVBGGR.js.map
132
+ //# sourceMappingURL=chunk-2F2PJ7D7.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/text.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { FsError } from '@deepseek-ai/dsh-fs'\n\nconst BINARY_SAMPLE_BYTES = 8192\n\n// dsh text semantics: a NUL in the leading sample or invalid UTF-8 is a\n// binary file, refused as FS_NOT_TEXT rather than decoded lossily (the\n// mirage facade's readFileText decodes with fatal: false, which the dsh\n// seam contract forbids).\nexport function decodeStrictText(bytes: Uint8Array, displayPath: string): string {\n if (bytes.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n throw new FsError(`cannot read \"${displayPath}\": binary file`, 'FS_NOT_TEXT')\n }\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n } catch (error: unknown) {\n throw new FsError(`cannot read \"${displayPath}\": invalid UTF-8 text`, 'FS_NOT_TEXT', {\n cause: error,\n })\n }\n}\n\nexport function normalizeLineEndings(value: string): string {\n return value.replaceAll('\\r\\n', '\\n')\n}\n\n// CRLF-majority sniff over a bounded sample, the same heuristic dsh's own\n// backends use to keep a rewritten file in its original line-ending style.\nexport function detectsCrlf(value: string): boolean {\n const sample = value.slice(0, 4096)\n const crlf = sample.split('\\r\\n').length - 1\n const lf = sample.split('\\n').length - 1 - crlf\n return crlf > lf\n}\n\nexport function restoreLineEndings(value: string, crlf: boolean): string {\n return crlf ? normalizeLineEndings(value).replaceAll('\\n', '\\r\\n') : value\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle.length === 0) return 0\n let count = 0\n let index = haystack.indexOf(needle)\n while (index !== -1) {\n count++\n index = haystack.indexOf(needle, index + needle.length)\n }\n return count\n}\n\n// Literal replacement with dsh's edit taxonomy: zero matches and an empty\n// oldString are FS_EDIT_NOT_FOUND, several matches without replaceAll is\n// FS_AMBIGUOUS_EDIT.\nexport function applyLiteralEdit(\n content: string,\n oldString: string,\n newString: string,\n replaceAll: boolean,\n displayPath: string,\n): string {\n const matches = countOccurrences(content, oldString)\n if (matches === 0) {\n throw new FsError(`cannot edit \"${displayPath}\": oldString not found`, 'FS_EDIT_NOT_FOUND')\n }\n if (matches > 1 && !replaceAll) {\n throw new FsError(\n `cannot edit \"${displayPath}\": oldString matches ${String(matches)} locations; pass replaceAll or a longer unique string`,\n 'FS_AMBIGUOUS_EDIT',\n )\n }\n return replaceAll\n ? content.replaceAll(oldString, newString)\n : content.replace(oldString, newString)\n}\n\n// Byte-accurate tail cap: keep the LAST maxBytes bytes of the encoded text,\n// re-aligned to a UTF-8 sequence boundary so the kept tail still decodes.\nexport function tailCap(text: string, maxBytes: number): { text: string; truncated: boolean } {\n const bytes = new TextEncoder().encode(text)\n if (bytes.byteLength <= maxBytes) return { text, truncated: false }\n let start = bytes.byteLength - maxBytes\n while (start < bytes.byteLength && ((bytes[start] ?? 0) & 0xc0) === 0x80) start++\n return {\n text: new TextDecoder('utf-8', { fatal: false }).decode(bytes.subarray(start)),\n truncated: true,\n }\n}\n"],"mappings":";AAcA,SAAS,eAAe;AAExB,IAAM,sBAAsB;AAMrB,SAAS,iBAAiB,OAAmB,aAA6B;AAC/E,MAAI,MAAM,SAAS,GAAG,mBAAmB,EAAE,SAAS,CAAC,GAAG;AACtD,UAAM,IAAI,QAAQ,gBAAgB,WAAW,kBAAkB,aAAa;AAAA,EAC9E;AACA,MAAI;AACF,WAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,KAAK;AAAA,EAC/D,SAAS,OAAgB;AACvB,UAAM,IAAI,QAAQ,gBAAgB,WAAW,yBAAyB,eAAe;AAAA,MACnF,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAEO,SAAS,qBAAqB,OAAuB;AAC1D,SAAO,MAAM,WAAW,QAAQ,IAAI;AACtC;AAIO,SAAS,YAAY,OAAwB;AAClD,QAAM,SAAS,MAAM,MAAM,GAAG,IAAI;AAClC,QAAM,OAAO,OAAO,MAAM,MAAM,EAAE,SAAS;AAC3C,QAAM,KAAK,OAAO,MAAM,IAAI,EAAE,SAAS,IAAI;AAC3C,SAAO,OAAO;AAChB;AAEO,SAAS,mBAAmB,OAAe,MAAuB;AACvE,SAAO,OAAO,qBAAqB,KAAK,EAAE,WAAW,MAAM,MAAM,IAAI;AACvE;AAEA,SAAS,iBAAiB,UAAkB,QAAwB;AAClE,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI,QAAQ;AACZ,MAAI,QAAQ,SAAS,QAAQ,MAAM;AACnC,SAAO,UAAU,IAAI;AACnB;AACA,YAAQ,SAAS,QAAQ,QAAQ,QAAQ,OAAO,MAAM;AAAA,EACxD;AACA,SAAO;AACT;AAKO,SAAS,iBACd,SACA,WACA,WACA,YACA,aACQ;AACR,QAAM,UAAU,iBAAiB,SAAS,SAAS;AACnD,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI,QAAQ,gBAAgB,WAAW,0BAA0B,mBAAmB;AAAA,EAC5F;AACA,MAAI,UAAU,KAAK,CAAC,YAAY;AAC9B,UAAM,IAAI;AAAA,MACR,gBAAgB,WAAW,wBAAwB,OAAO,OAAO,CAAC;AAAA,MAClE;AAAA,IACF;AAAA,EACF;AACA,SAAO,aACH,QAAQ,WAAW,WAAW,SAAS,IACvC,QAAQ,QAAQ,WAAW,SAAS;AAC1C;AAIO,SAAS,QAAQ,MAAc,UAAwD;AAC5F,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,IAAI;AAC3C,MAAI,MAAM,cAAc,SAAU,QAAO,EAAE,MAAM,WAAW,MAAM;AAClE,MAAI,QAAQ,MAAM,aAAa;AAC/B,SAAO,QAAQ,MAAM,gBAAgB,MAAM,KAAK,KAAK,KAAK,SAAU,IAAM;AAC1E,SAAO;AAAA,IACL,MAAM,IAAI,YAAY,SAAS,EAAE,OAAO,MAAM,CAAC,EAAE,OAAO,MAAM,SAAS,KAAK,CAAC;AAAA,IAC7E,WAAW;AAAA,EACb;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/text.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { FsError } from '@deepseek-ai/dsh-fs'\n\nconst BINARY_SAMPLE_BYTES = 8192\n\n// dsh text semantics: a NUL in the leading sample or invalid UTF-8 is a\n// binary file, refused as FS_NOT_TEXT rather than decoded lossily (the\n// mirage facade's readFileText decodes with fatal: false, which the dsh\n// seam contract forbids).\nexport function decodeStrictText(bytes: Uint8Array, displayPath: string): string {\n if (bytes.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n throw new FsError(`cannot read \"${displayPath}\": binary file`, 'FS_NOT_TEXT')\n }\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n } catch (error: unknown) {\n throw new FsError(`cannot read \"${displayPath}\": invalid UTF-8 text`, 'FS_NOT_TEXT', {\n cause: error,\n })\n }\n}\n\nexport function normalizeLineEndings(value: string): string {\n return value.replaceAll('\\r\\n', '\\n')\n}\n\n// CRLF-majority sniff over a bounded sample, the same heuristic dsh's own\n// backends use to keep a rewritten file in its original line-ending style.\nexport function detectsCrlf(value: string): boolean {\n const sample = value.slice(0, 4096)\n const crlf = sample.split('\\r\\n').length - 1\n const lf = sample.split('\\n').length - 1 - crlf\n return crlf > lf\n}\n\nexport function restoreLineEndings(value: string, crlf: boolean): string {\n return crlf ? normalizeLineEndings(value).replaceAll('\\n', '\\r\\n') : value\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle.length === 0) return 0\n let count = 0\n let index = haystack.indexOf(needle)\n while (index !== -1) {\n count++\n index = haystack.indexOf(needle, index + needle.length)\n }\n return count\n}\n\n// Literal replacement with dsh's edit taxonomy: zero matches and an empty\n// oldString are FS_EDIT_NOT_FOUND, several matches without replaceAll is\n// FS_AMBIGUOUS_EDIT.\nexport function applyLiteralEdit(\n content: string,\n oldString: string,\n newString: string,\n replaceAll: boolean,\n displayPath: string,\n): string {\n const matches = countOccurrences(content, oldString)\n if (matches === 0) {\n throw new FsError(`cannot edit \"${displayPath}\": oldString not found`, 'FS_EDIT_NOT_FOUND')\n }\n if (matches > 1 && !replaceAll) {\n throw new FsError(\n `cannot edit \"${displayPath}\": oldString matches ${String(matches)} locations; pass replaceAll or a longer unique string`,\n 'FS_AMBIGUOUS_EDIT',\n )\n }\n return replaceAll\n ? content.replaceAll(oldString, newString)\n : content.replace(oldString, newString)\n}\n\n// The first index at or after `from` that starts a UTF-8 sequence, so a tail\n// taken from there decodes as characters rather than as replacement marks.\nfunction charBoundary(bytes: Uint8Array, from: number): number {\n let start = from\n while (start < bytes.byteLength && ((bytes[start] ?? 0) & 0xc0) === 0x80) start++\n return start\n}\n\n// Byte-accurate tail cap: keep the LAST maxBytes bytes of the encoded text,\n// re-aligned to a UTF-8 sequence boundary so the kept tail still decodes.\nexport function tailCap(text: string, maxBytes: number): { text: string; truncated: boolean } {\n const bytes = new TextEncoder().encode(text)\n if (bytes.byteLength <= maxBytes) return { text, truncated: false }\n const start = charBoundary(bytes, bytes.byteLength - maxBytes)\n return {\n text: new TextDecoder('utf-8', { fatal: false }).decode(bytes.subarray(start)),\n truncated: true,\n }\n}\n\n/**\n * A bounded backlog of output bytes: the newest `budget` bytes, with the\n * oldest dropped as they overflow.\n *\n * Held as bytes rather than as a string because the string form had to\n * re-encode everything already buffered on every append to measure it,\n * which is quadratic in a command's output and lands on the same event\n * loop that serves the workspace. Appending here costs the chunk, not\n * the backlog.\n */\nexport class TailBuffer {\n private readonly budget: number\n private parts: Uint8Array[] = []\n private bytes = 0\n\n constructor(budget: number) {\n this.budget = budget\n }\n\n /**\n * Append a chunk, dropping the oldest bytes that no longer fit.\n *\n * @param data the bytes to append.\n * @returns true when bytes were dropped to make room.\n */\n append(data: Uint8Array): boolean {\n this.parts.push(data)\n this.bytes += data.byteLength\n if (this.bytes <= this.budget) return false\n let excess = this.bytes - this.budget\n while (excess > 0) {\n const head = this.parts[0]\n if (head === undefined) break\n if (head.byteLength <= excess) {\n this.parts.shift()\n excess -= head.byteLength\n this.bytes -= head.byteLength\n } else {\n this.parts[0] = head.subarray(excess)\n this.bytes -= excess\n excess = 0\n }\n }\n return true\n }\n\n /**\n * Drain everything held, decoded as text.\n *\n * The head may sit mid-character, since dropping is byte-exact, so it\n * is re-aligned before decoding rather than rendered as a replacement\n * mark.\n *\n * @returns the buffered text; the buffer is left empty.\n */\n take(): string {\n if (this.parts.length === 0) return ''\n const joined = new Uint8Array(this.bytes)\n let at = 0\n for (const part of this.parts) {\n joined.set(part, at)\n at += part.byteLength\n }\n this.parts = []\n this.bytes = 0\n return new TextDecoder('utf-8', { fatal: false }).decode(\n joined.subarray(charBoundary(joined, 0)),\n )\n }\n}\n"],"mappings":"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","names":[]}