@struktoai/mirage-opencode 0.0.8-alpha.1 → 0.0.8
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 +140 -46
- package/dist/{chunk-XH4ZMDIL.js → chunk-5RWDQVUW.js} +12 -11
- package/dist/chunk-5RWDQVUW.js.map +1 -0
- package/dist/plugin.d.ts +18 -1
- package/dist/plugin.js +1 -3
- package/dist/server.js +1 -1
- package/package.json +7 -7
- package/dist/chunk-XH4ZMDIL.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
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
6
|
</picture>
|
|
7
7
|
</a>
|
|
8
|
+
<a href="https://agentvfs.org">
|
|
9
|
+
<picture>
|
|
10
|
+
<source media="(max-width: 767px) and (prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/agentvfs-banner-narrow-dark.svg">
|
|
11
|
+
<source media="(max-width: 767px)" srcset="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/agentvfs-banner-narrow-light.svg">
|
|
12
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/agentvfs-banner-dark.svg">
|
|
13
|
+
<img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/agentvfs-banner-light.svg" width="100%" alt="AgentVFS: the open, vendor-neutral virtual filesystem interface for AI agents" />
|
|
14
|
+
</picture>
|
|
15
|
+
</a>
|
|
8
16
|
</h1>
|
|
9
17
|
|
|
10
18
|
<p align="center">
|
|
@@ -16,6 +24,8 @@
|
|
|
16
24
|
<img src="https://img.shields.io/badge/made by-strukto.ai-0C0C0C?labelColor=F0ECE2" /></a>
|
|
17
25
|
<a href="https://github.com/strukto-ai/mirage/blob/main/LICENSE" alt="License">
|
|
18
26
|
<img src="https://img.shields.io/badge/license-Apache--2.0-0C0C0C?labelColor=F0ECE2" /></a>
|
|
27
|
+
<a href="https://doi.org/10.5281/zenodo.23504502" alt="DOI">
|
|
28
|
+
<img src="https://img.shields.io/badge/DOI-10.5281%2Fzenodo.23504502-0C0C0C?labelColor=F0ECE2" alt="DOI 10.5281/zenodo.23504502" /></a>
|
|
19
29
|
<a href="https://discord.gg/u8BPQ65KsS" alt="Discord">
|
|
20
30
|
<img src="https://img.shields.io/badge/discord-join-0C0C0C?labelColor=F0ECE2&logo=discord&logoColor=0C0C0C" /></a>
|
|
21
31
|
<br/>
|
|
@@ -40,7 +50,7 @@
|
|
|
40
50
|
<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
51
|
</p>
|
|
42
52
|
|
|
43
|
-
Mirage is **a Virtual Terminal for AI Agents
|
|
53
|
+
Mirage is **a Virtual Terminal for AI Agents**: one shell over data, CLIs and runtimes. Unix tools reach every service, and `grep` runs on each service's own search, such as Gmail's or Slack's. Mirage routes each command to the runtime that fits it, so most lines run in-process and only heavy work reaches a sandbox, which cuts infrastructure cost and tokens. The terminal itself is light: a workspace takes **about 100 KB** of memory and each new session **about 10 KB**, so one process serves many agents at once. Profiles decide what an agent can run and even what it can see, and scriptable policies refuse anything those rules cannot express.
|
|
44
54
|
|
|
45
55
|
<p align="center">
|
|
46
56
|
<picture>
|
|
@@ -54,24 +64,56 @@ Here is an example of launching Mirage inside an application:
|
|
|
54
64
|
```python
|
|
55
65
|
ws = Workspace(
|
|
56
66
|
{
|
|
57
|
-
"/
|
|
58
|
-
"/redis": (RedisVFS(url=redis_url), MountMode.WRITE),
|
|
67
|
+
"/s3": (S3VFS(S3Config(bucket="my-bucket")), MountMode.WRITE),
|
|
59
68
|
"/slack": (SlackVFS(SlackConfig(token=slack_bot_token)), MountMode.EXEC),
|
|
69
|
+
"/gmail": (GmailVFS(GmailConfig(**google_credentials)), MountMode.READ),
|
|
60
70
|
},
|
|
61
|
-
#
|
|
62
|
-
|
|
71
|
+
# Install the Slack CLI, run by name and listed by `man` and `which` like any program
|
|
72
|
+
clis={"slack": (SLACK, {"token": slack_bot_token})},
|
|
73
|
+
# the analyst profile hides /s3/secrets and denies posting to Slack and curl
|
|
74
|
+
profiles={
|
|
75
|
+
"analyst": {
|
|
76
|
+
"paths": {"hide": ["/s3/secrets"]},
|
|
77
|
+
"commands": {"deny": ["slack send-message", "curl"]},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
runtimes=[
|
|
81
|
+
MontyRuntime(captures=["python", "python3"]),
|
|
82
|
+
E2BRuntime(
|
|
83
|
+
captures=["python", "python3"],
|
|
84
|
+
config={"sandbox_id": sandbox_id, "mount_type": "ssh"},
|
|
85
|
+
),
|
|
86
|
+
],
|
|
87
|
+
# route the underlying computation: numpy lines run in the e2b sandbox, the rest on monty
|
|
88
|
+
route_policy=lambda ctx: "e2b" if "numpy" in ctx.line else None,
|
|
63
89
|
)
|
|
64
90
|
|
|
65
|
-
#
|
|
66
|
-
await ws.
|
|
91
|
+
# Add an agent using the analyst profile
|
|
92
|
+
alice = await ws.session("alice", profile="analyst")
|
|
93
|
+
|
|
94
|
+
# one grep sweeps S3, Slack and Gmail, on each service's own search where it has one
|
|
95
|
+
await alice.shell("grep -rlw invoice /s3 /slack /gmail")
|
|
67
96
|
|
|
68
|
-
# run a script that lives in Slack, file the report into
|
|
69
|
-
await
|
|
97
|
+
# run a script that lives in Slack on monty, file the report into S3
|
|
98
|
+
await alice.shell("python3 /slack/channels/general__C0123/2026-10-21/files/report__F0456.py > /s3/report.txt")
|
|
70
99
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
# numpy is not in monty, so the route policy sends this line to e2b
|
|
101
|
+
await alice.shell("python3 -c 'import numpy; print(numpy.arange(10).sum())'")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
And what `alice` sees in the shell:
|
|
105
|
+
|
|
106
|
+
```console
|
|
107
|
+
$ grep -rlw invoice /s3 /slack /gmail
|
|
108
|
+
/s3/billing/2026-10.csv
|
|
109
|
+
/gmail/INBOX/2026-10-21/Invoice_1042__18f3a9c2b7d1e0f4.gmail.json
|
|
110
|
+
$ ls /s3
|
|
111
|
+
billing
|
|
112
|
+
report.txt
|
|
113
|
+
$ cat /s3/secrets/aws.txt
|
|
114
|
+
cat: /s3/secrets/aws.txt: No such file or directory
|
|
115
|
+
$ slack send-message --channel general --text "report is up"
|
|
116
|
+
slack: Permission denied
|
|
75
117
|
```
|
|
76
118
|
|
|
77
119
|
## About
|
|
@@ -81,6 +123,7 @@ await ws.shell('slack send-message --channel general --text "report is up"')
|
|
|
81
123
|
- **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
124
|
- **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
125
|
- **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.
|
|
126
|
+
- **Lightweight by design.** A workspace, the virtual terminal itself, takes about 100 KB of memory and each new session about 10 KB, so one process serves many agents without a container or VM per agent.
|
|
84
127
|
- **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
128
|
- **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
129
|
- **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.
|
|
@@ -158,8 +201,8 @@ in Mirage can resolve straight to a credential held in one of them.
|
|
|
158
201
|
|
|
159
202
|
## Installation
|
|
160
203
|
|
|
161
|
-
- **Python** ≥ 3.
|
|
162
|
-
- **Node.js**
|
|
204
|
+
- **Python** ≥ 3.12 for the `mirage-ai` package and the `mirage` CLI
|
|
205
|
+
- **Node.js** 22.20+ on Node 22, 24.12+ on Node 24, or 25+ for the TypeScript SDK
|
|
163
206
|
|
|
164
207
|
### Python
|
|
165
208
|
|
|
@@ -181,58 +224,109 @@ Both runtime packages pull in `@struktoai/mirage-core` automatically.
|
|
|
181
224
|
### CLI
|
|
182
225
|
|
|
183
226
|
```bash
|
|
184
|
-
curl -fsSL https://strukto.ai/mirage/install.sh | sh
|
|
185
|
-
# or
|
|
186
227
|
npm install -g @struktoai/mirage-cli
|
|
187
228
|
# or
|
|
188
|
-
uvx mirage-ai
|
|
229
|
+
uvx --from mirage-ai mirage
|
|
189
230
|
# or
|
|
190
231
|
npx @struktoai/mirage-cli
|
|
191
232
|
```
|
|
192
233
|
|
|
193
|
-
|
|
234
|
+
To get started, see the quickstarts for
|
|
235
|
+
[Python](https://docs.mirage.strukto.ai/python/quickstart),
|
|
236
|
+
[TypeScript](https://docs.mirage.strukto.ai/typescript/quickstart) and the
|
|
237
|
+
[CLI](https://docs.mirage.strukto.ai/home/access/cli).
|
|
194
238
|
|
|
195
|
-
|
|
239
|
+
## Scriptable Policy
|
|
240
|
+
|
|
241
|
+
A profile can name policy scripts for what its rules cannot express. A script
|
|
242
|
+
reads the command as JSON, can open the files it names, and prints a deny, an
|
|
243
|
+
ask or nothing. This one disables `rm`, and a Python script whose code
|
|
244
|
+
deletes files:
|
|
196
245
|
|
|
197
246
|
```python
|
|
198
|
-
|
|
199
|
-
from mirage.vfs.ram import RAMVFS
|
|
200
|
-
from mirage.vfs.s3 import S3Config, S3VFS
|
|
247
|
+
import json, os
|
|
201
248
|
|
|
202
|
-
|
|
203
|
-
"/data": RAMVFS(),
|
|
204
|
-
"/s3": S3VFS(S3Config(bucket="my-bucket")),
|
|
205
|
-
})
|
|
249
|
+
ctx = json.loads(os.environ["MIRAGE_POLICY_CTX"])
|
|
206
250
|
|
|
207
|
-
await ws.shell("cp /s3/report.csv /data/report.csv")
|
|
208
|
-
await ws.shell("grep alert /s3/data/log.jsonl | wc -l")
|
|
209
251
|
|
|
210
|
-
|
|
211
|
-
|
|
252
|
+
def deletes(path):
|
|
253
|
+
try:
|
|
254
|
+
code = open(path).read()
|
|
255
|
+
except OSError:
|
|
256
|
+
return False
|
|
257
|
+
return any(call in code for call in ("os.remove", "os.unlink", "shutil.rmtree"))
|
|
212
258
|
|
|
213
|
-
### TypeScript
|
|
214
259
|
|
|
215
|
-
|
|
216
|
-
|
|
260
|
+
if ctx["command"] == "rm" or (
|
|
261
|
+
ctx["command"].startswith("python") and any(deletes(p) for p in ctx["paths"])
|
|
262
|
+
):
|
|
263
|
+
print(json.dumps({"kind": "deny", "reason": "deleting files is disabled"}))
|
|
264
|
+
```
|
|
217
265
|
|
|
218
|
-
|
|
219
|
-
'/data': new RAMVFS(),
|
|
220
|
-
'/s3': new S3VFS({ bucket: 'my-bucket' }),
|
|
221
|
-
})
|
|
266
|
+
Register it in a profile, here to run on Monty before every command:
|
|
222
267
|
|
|
223
|
-
|
|
224
|
-
|
|
268
|
+
```python
|
|
269
|
+
ws = Workspace(
|
|
270
|
+
{"/s3": (S3VFS(S3Config(bucket="my-bucket")), MountMode.WRITE)},
|
|
271
|
+
profiles={
|
|
272
|
+
"analyst": {
|
|
273
|
+
"policy": {
|
|
274
|
+
"pre_command": [{"script": ScriptSource(Path("guard.py").read_text()), "runtime": "monty"}],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
profile="analyst",
|
|
279
|
+
)
|
|
280
|
+
```
|
|
225
281
|
|
|
226
|
-
|
|
282
|
+
```console
|
|
283
|
+
$ cat /s3/clean.py
|
|
284
|
+
import os
|
|
285
|
+
os.remove("/s3/report.txt")
|
|
286
|
+
$ rm /s3/report.txt
|
|
287
|
+
rm: Permission denied
|
|
288
|
+
$ python3 /s3/clean.py
|
|
289
|
+
python3: Permission denied
|
|
227
290
|
```
|
|
228
291
|
|
|
229
|
-
|
|
292
|
+
See [policy scripts](https://docs.mirage.strukto.ai/home/policy/scripts) for
|
|
293
|
+
the other stages a script can answer at.
|
|
294
|
+
|
|
295
|
+
## Embedded Terminal
|
|
296
|
+
|
|
297
|
+
Most of the time an agent does not need a sandbox, only a terminal-like
|
|
298
|
+
interface, and Mirage embeds one in an application server. Each terminal is a
|
|
299
|
+
Mirage session with its own working directory, environment and history, and Python
|
|
300
|
+
runs in-process on Monty, so a terminal starts with no container or VM. Here
|
|
301
|
+
it is in Express; the FastAPI version is in the
|
|
302
|
+
[docs](https://docs.mirage.strukto.ai/home/examples/embedded-terminal).
|
|
230
303
|
|
|
231
304
|
```bash
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
305
|
+
npm install @struktoai/mirage-node @pydantic/monty express
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import express from 'express'
|
|
310
|
+
import { MontyRuntime, MountMode, RAMVFS, Workspace } from '@struktoai/mirage-node'
|
|
311
|
+
|
|
312
|
+
const ws = new Workspace(
|
|
313
|
+
{ '/work': new RAMVFS() },
|
|
314
|
+
{ mode: MountMode.EXEC, runtimes: [new MontyRuntime({ captures: ['python', 'python3'] })] },
|
|
315
|
+
)
|
|
316
|
+
const app = express()
|
|
317
|
+
app.use(express.json())
|
|
318
|
+
|
|
319
|
+
app.post('/sessions/:id', (req, res) => {
|
|
320
|
+
ws.createSession(req.params.id)
|
|
321
|
+
res.sendStatus(201)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
app.post('/sessions/:id/run', async (req, res) => {
|
|
325
|
+
const r = await ws.shell(req.body.command, { sessionId: req.params.id })
|
|
326
|
+
res.json({ stdout: r.stdoutText, stderr: r.stderrText, exitCode: r.exitCode })
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
app.listen(3000, '127.0.0.1')
|
|
236
330
|
```
|
|
237
331
|
|
|
238
332
|
## Contributors
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { ensureParents } from "@struktoai/mirage-core/workspace/tools/tool_operations";
|
|
3
3
|
import { tool } from "@opencode-ai/plugin";
|
|
4
4
|
import { encodeBase64 } from "@struktoai/mirage-core/utils/base64";
|
|
5
|
+
import { singleQuote } from "@struktoai/mirage-core/utils/quote";
|
|
5
6
|
import { FileVersionTracker } from "@struktoai/mirage-core/workspace/tools/file_version";
|
|
6
7
|
import { Session } from "@struktoai/mirage-core/workspace/workspace/workspace";
|
|
7
8
|
import { readWorkspaceFile } from "@struktoai/mirage-core/workspace/tools/read_file";
|
|
8
9
|
import { decode, replaceText, withRefusal } from "@struktoai/mirage-core/workspace/tools/io_text";
|
|
9
|
-
import { StaleMirageFileError } from "@struktoai/mirage-core/workspace/tools/file_version";
|
|
10
10
|
var z = tool.schema;
|
|
11
11
|
function isResolver(ws) {
|
|
12
12
|
return typeof ws === "function";
|
|
@@ -150,7 +150,7 @@ function mirageTools(ws, options = {}) {
|
|
|
150
150
|
},
|
|
151
151
|
execute: async ({ command }, ctx) => {
|
|
152
152
|
const w = await resolveWs(ws, ctx);
|
|
153
|
-
const io = await w.shell(command, shellOptions);
|
|
153
|
+
const io = await w.shell(command, { ...shellOptions, signal: ctx.abort });
|
|
154
154
|
const parts = [];
|
|
155
155
|
if (io.stdoutText.length > 0) parts.push(io.stdoutText);
|
|
156
156
|
if (io.stderrText.length > 0) parts.push(io.stderrText);
|
|
@@ -166,10 +166,10 @@ function mirageTools(ws, options = {}) {
|
|
|
166
166
|
execute: async ({ pattern, path }, ctx) => {
|
|
167
167
|
const w = await resolveWs(ws, ctx);
|
|
168
168
|
const root = path ?? "/";
|
|
169
|
-
const io = await w.shell(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
169
|
+
const io = await w.shell(`find ${singleQuote(root)} -name ${singleQuote(pattern)}`, {
|
|
170
|
+
...shellOptions,
|
|
171
|
+
signal: ctx.abort
|
|
172
|
+
});
|
|
173
173
|
return io.stdoutText.trim();
|
|
174
174
|
}
|
|
175
175
|
});
|
|
@@ -182,8 +182,10 @@ function mirageTools(ws, options = {}) {
|
|
|
182
182
|
execute: async ({ pattern, path }, ctx) => {
|
|
183
183
|
const w = await resolveWs(ws, ctx);
|
|
184
184
|
const root = path ?? "/";
|
|
185
|
-
const
|
|
186
|
-
|
|
185
|
+
const io = await w.shell(`grep -rn -e ${singleQuote(pattern)} ${singleQuote(root)}`, {
|
|
186
|
+
...shellOptions,
|
|
187
|
+
signal: ctx.abort
|
|
188
|
+
});
|
|
187
189
|
return io.stdoutText.trim();
|
|
188
190
|
}
|
|
189
191
|
});
|
|
@@ -195,7 +197,6 @@ function miragePlugin(ws, options = {}) {
|
|
|
195
197
|
|
|
196
198
|
export {
|
|
197
199
|
mirageTools,
|
|
198
|
-
miragePlugin
|
|
199
|
-
StaleMirageFileError
|
|
200
|
+
miragePlugin
|
|
200
201
|
};
|
|
201
|
-
//# sourceMappingURL=chunk-
|
|
202
|
+
//# sourceMappingURL=chunk-5RWDQVUW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin.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 { ensureParents } from '@struktoai/mirage-core/workspace/tools/tool_operations'\nimport { tool, type Plugin, type ToolContext, type ToolDefinition } from '@opencode-ai/plugin'\nimport type { Workspace } from '@struktoai/mirage-node'\nimport { encodeBase64 } from '@struktoai/mirage-core/utils/base64'\nimport { singleQuote } from '@struktoai/mirage-core/utils/quote'\nimport { FileVersionTracker } from '@struktoai/mirage-core/workspace/tools/file_version'\nimport { Session } from '@struktoai/mirage-core/workspace/workspace/workspace'\nimport { readWorkspaceFile } from '@struktoai/mirage-core/workspace/tools/read_file'\nimport { decode, replaceText, withRefusal } from '@struktoai/mirage-core/workspace/tools/io_text'\n\nconst z = tool.schema\n\n/**\n * A function that picks the workspace for a tool call from its OpenCode\n * `ToolContext`.\n */\nexport type WsResolver = (ctx: ToolContext) => Workspace | Promise<Workspace>\n\n/**\n * A workspace, or a `WsResolver` that picks one per tool call.\n */\nexport type WsLike = Workspace | WsResolver\n\n/**\n * Options for `mirageTools` and `miragePlugin`: `staleWriteProtection`\n * (default `true`) and `sessionId`, the Mirage session every tool acts as.\n */\nexport interface MirageOpenCodeOptions {\n staleWriteProtection?: boolean\n /**\n * The mirage session the tools act as, so its profile judges every\n * call; the workspace's default session when absent.\n */\n sessionId?: string\n}\n\ntype SessionTrackers = Map<string, FileVersionTracker>\n\nfunction isResolver(ws: WsLike): ws is WsResolver {\n return typeof ws === 'function'\n}\n\nasync function resolveWs(ws: WsLike, ctx: ToolContext): Promise<Workspace> {\n return isResolver(ws) ? ws(ctx) : ws\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nfunction trackerFor(\n trackers: WeakMap<Workspace, SessionTrackers>,\n ws: Workspace,\n ctx: ToolContext,\n enabled: boolean,\n sessionId: string | undefined,\n): FileVersionTracker {\n let sessions = trackers.get(ws)\n if (sessions === undefined) {\n sessions = new Map<string, FileVersionTracker>()\n trackers.set(ws, sessions)\n }\n let tracker = sessions.get(ctx.sessionID)\n if (tracker === undefined) {\n tracker = new FileVersionTracker(\n sessionId === undefined ? ws.vfs : new Session(ws, sessionId).vfs,\n enabled,\n )\n sessions.set(ctx.sessionID, tracker)\n }\n return tracker\n}\n\n/**\n * Returns OpenCode tool definitions for `read`, `write`, `edit`, `ls`,\n * `bash`, `glob` and `grep`, backed by `ws`.\n */\nexport function mirageTools(\n ws: WsLike,\n options: MirageOpenCodeOptions = {},\n): Record<string, ToolDefinition> {\n const trackers = new WeakMap<Workspace, SessionTrackers>()\n const staleWriteProtection = options.staleWriteProtection ?? true\n const sessionId = options.sessionId\n const shellOptions = sessionId === undefined ? {} : { sessionId }\n const read = tool({\n description:\n 'Read a file. Returns UTF-8 text for source/data files, attaches PDFs and images for multimodal models, and returns metadata for other binary files.',\n args: {\n filePath: z.string().describe('Absolute path of the file to read.'),\n },\n execute: async ({ filePath }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const versions = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId)\n try {\n const result = await readWorkspaceFile(versions.vfs, filePath, versions.read.bind(versions))\n if (result.kind === 'text') return result.content\n if (result.kind === 'image' || result.kind === 'file') {\n const filename = result.path.split('/').pop() ?? result.path\n return {\n output: `${result.path} (${result.mimeType}, ${String(result.bytes)} bytes)`,\n attachments: [\n {\n type: 'file' as const,\n mime: result.mimeType,\n url: `data:${result.mimeType};base64,${encodeBase64(result.data)}`,\n filename,\n },\n ],\n }\n }\n return result.note\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n },\n })\n\n const write = tool({\n description: 'Write content to a file. Creates missing parent directories.',\n args: {\n filePath: z.string().describe('Absolute path of the file to write.'),\n content: z.string().describe('UTF-8 text content to write.'),\n },\n execute: async ({ filePath, content }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const versions = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId)\n try {\n await ensureParents(versions.vfs, filePath)\n await versions.write(filePath, content)\n return `Wrote ${String(content.length)} bytes to ${filePath}`\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n },\n })\n\n const edit = tool({\n description:\n 'Replace a string inside an existing file. Errors if the string appears more than once unless replaceAll is true.',\n args: {\n filePath: z.string().describe('Absolute path of the file to edit.'),\n oldString: z.string().describe('The exact string to replace.'),\n newString: z.string().describe('The replacement string.'),\n replaceAll: z\n .boolean()\n .optional()\n .describe('Replace every occurrence rather than requiring a unique match.'),\n },\n execute: async ({ filePath, oldString, newString, replaceAll }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const versions = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId)\n let current: string\n try {\n current = decode(await versions.readForEdit(filePath))\n } catch (err) {\n if (await versions.vfs.exists(filePath)) return `Error: ${errMsg(err)}`\n return `Error: file '${filePath}' not found`\n }\n const [next, count] = replaceText(current, oldString, newString, replaceAll === true)\n if (count === 0) {\n return `Error: string not found in file: '${oldString}'`\n }\n if (count > 1 && replaceAll !== true) {\n return `Error: string '${oldString}' appears ${String(count)} times. Use replaceAll=true`\n }\n try {\n await versions.writeEdit(filePath, next)\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n const occurrences = replaceAll === true ? count : 1\n return `Edited ${filePath} (${String(occurrences)} occurrence${occurrences === 1 ? '' : 's'})`\n },\n })\n\n const ls = tool({\n description: 'List entries of a directory.',\n args: {\n path: z.string().describe('Absolute directory path.'),\n },\n execute: async ({ path }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const vfs = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId).vfs\n let entries: string[]\n try {\n entries = await vfs.readdir(path)\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n const lines: string[] = []\n for (const entry of entries) {\n const isDir = await vfs.isDir(entry)\n lines.push(isDir ? `${entry}/` : entry)\n }\n return lines.join('\\n')\n },\n })\n\n const bash = tool({\n description: 'Execute a shell command and return stdout, stderr, and exit code.',\n args: {\n command: z.string().describe('The shell command to execute.'),\n },\n execute: async ({ command }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const io = await w.shell(command, { ...shellOptions, signal: ctx.abort })\n const parts: string[] = []\n if (io.stdoutText.length > 0) parts.push(io.stdoutText)\n if (io.stderrText.length > 0) parts.push(io.stderrText)\n return withRefusal(parts.join('\\n'), io.refusal).trim()\n },\n })\n\n const glob = tool({\n description: 'Find files matching a name pattern.',\n args: {\n pattern: z.string().describe('Filename pattern (e.g. \"*.ts\").'),\n path: z.string().optional().describe('Directory to search under. Defaults to /.'),\n },\n execute: async ({ pattern, path }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const root = path ?? '/'\n const io = await w.shell(`find ${singleQuote(root)} -name ${singleQuote(pattern)}`, {\n ...shellOptions,\n signal: ctx.abort,\n })\n return io.stdoutText.trim()\n },\n })\n\n const grep = tool({\n description: 'Search for a regex pattern in files.',\n args: {\n pattern: z.string().describe('Pattern to search for.'),\n path: z.string().optional().describe('Directory or file to search under. Defaults to /.'),\n },\n execute: async ({ pattern, path }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const root = path ?? '/'\n const io = await w.shell(`grep -rn -e ${singleQuote(pattern)} ${singleQuote(root)}`, {\n ...shellOptions,\n signal: ctx.abort,\n })\n return io.stdoutText.trim()\n },\n })\n\n return { read, write, edit, ls, bash, glob, grep }\n}\n\n/**\n * Returns a native OpenCode plugin whose tools are `mirageTools(ws, options)`.\n */\nexport function miragePlugin(ws: WsLike, options: MirageOpenCodeOptions = {}): Plugin {\n return () => Promise.resolve({ tool: mirageTools(ws, options) })\n}\n"],"mappings":"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","names":[]}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { ToolContext, Plugin, ToolDefinition } from '@opencode-ai/plugin';
|
|
2
2
|
import { Workspace } from '@struktoai/mirage-node';
|
|
3
|
-
export { StaleMirageFileError } from '@struktoai/mirage-core/workspace/tools/file_version';
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* A function that picks the workspace for a tool call from its OpenCode
|
|
6
|
+
* `ToolContext`.
|
|
7
|
+
*/
|
|
5
8
|
type WsResolver = (ctx: ToolContext) => Workspace | Promise<Workspace>;
|
|
9
|
+
/**
|
|
10
|
+
* A workspace, or a `WsResolver` that picks one per tool call.
|
|
11
|
+
*/
|
|
6
12
|
type WsLike = Workspace | WsResolver;
|
|
13
|
+
/**
|
|
14
|
+
* Options for `mirageTools` and `miragePlugin`: `staleWriteProtection`
|
|
15
|
+
* (default `true`) and `sessionId`, the Mirage session every tool acts as.
|
|
16
|
+
*/
|
|
7
17
|
interface MirageOpenCodeOptions {
|
|
8
18
|
staleWriteProtection?: boolean;
|
|
9
19
|
/**
|
|
@@ -12,7 +22,14 @@ interface MirageOpenCodeOptions {
|
|
|
12
22
|
*/
|
|
13
23
|
sessionId?: string;
|
|
14
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns OpenCode tool definitions for `read`, `write`, `edit`, `ls`,
|
|
27
|
+
* `bash`, `glob` and `grep`, backed by `ws`.
|
|
28
|
+
*/
|
|
15
29
|
declare function mirageTools(ws: WsLike, options?: MirageOpenCodeOptions): Record<string, ToolDefinition>;
|
|
30
|
+
/**
|
|
31
|
+
* Returns a native OpenCode plugin whose tools are `mirageTools(ws, options)`.
|
|
32
|
+
*/
|
|
16
33
|
declare function miragePlugin(ws: WsLike, options?: MirageOpenCodeOptions): Plugin;
|
|
17
34
|
|
|
18
35
|
export { type MirageOpenCodeOptions, type WsLike, type WsResolver, miragePlugin, mirageTools };
|
package/dist/plugin.js
CHANGED
package/dist/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@struktoai/mirage-opencode",
|
|
3
|
-
"version": "0.0.8
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Native Mirage filesystem tools for OpenCode",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mirage",
|
|
@@ -47,19 +47,19 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@struktoai/mirage-
|
|
51
|
-
"@struktoai/mirage-
|
|
52
|
-
"@struktoai/mirage-server": "0.0.8
|
|
50
|
+
"@struktoai/mirage-core": "0.0.8",
|
|
51
|
+
"@struktoai/mirage-node": "0.0.8",
|
|
52
|
+
"@struktoai/mirage-server": "0.0.8"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@opencode-ai/plugin": "^1.
|
|
55
|
+
"@opencode-ai/plugin": "^1.19.4"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@opencode-ai/plugin": "^1.
|
|
58
|
+
"@opencode-ai/plugin": "^1.19.5",
|
|
59
59
|
"@types/node": "^22.0.0",
|
|
60
60
|
"tsup": "^8.5.0",
|
|
61
61
|
"typescript": "^6.0.0",
|
|
62
|
-
"vitest": "^4.1.
|
|
62
|
+
"vitest": "^4.1.12"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "tsup",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.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 { ensureParents } from '@struktoai/mirage-core/workspace/tools/tool_operations'\nimport { tool, type Plugin, type ToolContext, type ToolDefinition } from '@opencode-ai/plugin'\nimport type { Workspace } from '@struktoai/mirage-node'\nimport { encodeBase64 } from '@struktoai/mirage-core/utils/base64'\nimport { FileVersionTracker } from '@struktoai/mirage-core/workspace/tools/file_version'\nimport { Session } from '@struktoai/mirage-core/workspace/workspace/workspace'\nimport { readWorkspaceFile } from '@struktoai/mirage-core/workspace/tools/read_file'\nimport { decode, replaceText, withRefusal } from '@struktoai/mirage-core/workspace/tools/io_text'\n\nconst z = tool.schema\n\nexport type WsResolver = (ctx: ToolContext) => Workspace | Promise<Workspace>\nexport type WsLike = Workspace | WsResolver\n\nexport interface MirageOpenCodeOptions {\n staleWriteProtection?: boolean\n /**\n * The mirage session the tools act as, so its profile judges every\n * call; the workspace's default session when absent.\n */\n sessionId?: string\n}\n\ntype SessionTrackers = Map<string, FileVersionTracker>\n\nfunction isResolver(ws: WsLike): ws is WsResolver {\n return typeof ws === 'function'\n}\n\nasync function resolveWs(ws: WsLike, ctx: ToolContext): Promise<Workspace> {\n return isResolver(ws) ? ws(ctx) : ws\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nfunction trackerFor(\n trackers: WeakMap<Workspace, SessionTrackers>,\n ws: Workspace,\n ctx: ToolContext,\n enabled: boolean,\n sessionId: string | undefined,\n): FileVersionTracker {\n let sessions = trackers.get(ws)\n if (sessions === undefined) {\n sessions = new Map<string, FileVersionTracker>()\n trackers.set(ws, sessions)\n }\n let tracker = sessions.get(ctx.sessionID)\n if (tracker === undefined) {\n tracker = new FileVersionTracker(\n sessionId === undefined ? ws.vfs : new Session(ws, sessionId).vfs,\n enabled,\n )\n sessions.set(ctx.sessionID, tracker)\n }\n return tracker\n}\n\nexport function mirageTools(\n ws: WsLike,\n options: MirageOpenCodeOptions = {},\n): Record<string, ToolDefinition> {\n const trackers = new WeakMap<Workspace, SessionTrackers>()\n const staleWriteProtection = options.staleWriteProtection ?? true\n const sessionId = options.sessionId\n const shellOptions = sessionId === undefined ? {} : { sessionId }\n const read = tool({\n description:\n 'Read a file. Returns UTF-8 text for source/data files, attaches PDFs and images for multimodal models, and returns metadata for other binary files.',\n args: {\n filePath: z.string().describe('Absolute path of the file to read.'),\n },\n execute: async ({ filePath }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const versions = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId)\n try {\n const result = await readWorkspaceFile(versions.vfs, filePath, versions.read.bind(versions))\n if (result.kind === 'text') return result.content\n if (result.kind === 'image' || result.kind === 'file') {\n const filename = result.path.split('/').pop() ?? result.path\n return {\n output: `${result.path} (${result.mimeType}, ${String(result.bytes)} bytes)`,\n attachments: [\n {\n type: 'file' as const,\n mime: result.mimeType,\n url: `data:${result.mimeType};base64,${encodeBase64(result.data)}`,\n filename,\n },\n ],\n }\n }\n return result.note\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n },\n })\n\n const write = tool({\n description: 'Write content to a file. Creates missing parent directories.',\n args: {\n filePath: z.string().describe('Absolute path of the file to write.'),\n content: z.string().describe('UTF-8 text content to write.'),\n },\n execute: async ({ filePath, content }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const versions = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId)\n try {\n await ensureParents(versions.vfs, filePath)\n await versions.write(filePath, content)\n return `Wrote ${String(content.length)} bytes to ${filePath}`\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n },\n })\n\n const edit = tool({\n description:\n 'Replace a string inside an existing file. Errors if the string appears more than once unless replaceAll is true.',\n args: {\n filePath: z.string().describe('Absolute path of the file to edit.'),\n oldString: z.string().describe('The exact string to replace.'),\n newString: z.string().describe('The replacement string.'),\n replaceAll: z\n .boolean()\n .optional()\n .describe('Replace every occurrence rather than requiring a unique match.'),\n },\n execute: async ({ filePath, oldString, newString, replaceAll }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const versions = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId)\n let current: string\n try {\n current = decode(await versions.readForEdit(filePath))\n } catch (err) {\n if (await versions.vfs.exists(filePath)) return `Error: ${errMsg(err)}`\n return `Error: file '${filePath}' not found`\n }\n const [next, count] = replaceText(current, oldString, newString, replaceAll === true)\n if (count === 0) {\n return `Error: string not found in file: '${oldString}'`\n }\n if (count > 1 && replaceAll !== true) {\n return `Error: string '${oldString}' appears ${String(count)} times. Use replaceAll=true`\n }\n try {\n await versions.writeEdit(filePath, next)\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n const occurrences = replaceAll === true ? count : 1\n return `Edited ${filePath} (${String(occurrences)} occurrence${occurrences === 1 ? '' : 's'})`\n },\n })\n\n const ls = tool({\n description: 'List entries of a directory.',\n args: {\n path: z.string().describe('Absolute directory path.'),\n },\n execute: async ({ path }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const vfs = trackerFor(trackers, w, ctx, staleWriteProtection, sessionId).vfs\n let entries: string[]\n try {\n entries = await vfs.readdir(path)\n } catch (err) {\n return `Error: ${errMsg(err)}`\n }\n const lines: string[] = []\n for (const entry of entries) {\n const isDir = await vfs.isDir(entry)\n lines.push(isDir ? `${entry}/` : entry)\n }\n return lines.join('\\n')\n },\n })\n\n const bash = tool({\n description: 'Execute a shell command and return stdout, stderr, and exit code.',\n args: {\n command: z.string().describe('The shell command to execute.'),\n },\n execute: async ({ command }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const io = await w.shell(command, shellOptions)\n const parts: string[] = []\n if (io.stdoutText.length > 0) parts.push(io.stdoutText)\n if (io.stderrText.length > 0) parts.push(io.stderrText)\n return withRefusal(parts.join('\\n'), io.refusal).trim()\n },\n })\n\n const glob = tool({\n description: 'Find files matching a name pattern.',\n args: {\n pattern: z.string().describe('Filename pattern (e.g. \"*.ts\").'),\n path: z.string().optional().describe('Directory to search under. Defaults to /.'),\n },\n execute: async ({ pattern, path }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const root = path ?? '/'\n const io = await w.shell(\n `find ${root} -name '${pattern.replace(/'/g, \"'\\\\''\")}'`,\n shellOptions,\n )\n return io.stdoutText.trim()\n },\n })\n\n const grep = tool({\n description: 'Search for a regex pattern in files.',\n args: {\n pattern: z.string().describe('Pattern to search for.'),\n path: z.string().optional().describe('Directory or file to search under. Defaults to /.'),\n },\n execute: async ({ pattern, path }, ctx) => {\n const w = await resolveWs(ws, ctx)\n const root = path ?? '/'\n const escaped = pattern.replace(/'/g, \"'\\\\''\")\n const io = await w.shell(`grep -rn '${escaped}' ${root}`, shellOptions)\n return io.stdoutText.trim()\n },\n })\n\n return { read, write, edit, ls, bash, glob, grep }\n}\n\nexport function miragePlugin(ws: WsLike, options: MirageOpenCodeOptions = {}): Plugin {\n return () => Promise.resolve({ tool: mirageTools(ws, options) })\n}\n\nexport { StaleMirageFileError } from '@struktoai/mirage-core/workspace/tools/file_version'\n"],"mappings":"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","names":[]}
|