anymous 1.1.5 → 1.2.0

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.
@@ -163,83 +163,75 @@ const layer = Layer.effect(
163
163
  $: typeof Bun === "undefined" ? undefined : Bun.$,
164
164
  }
165
165
 
166
- const internalInits = yield* Effect.all(
167
- (flags.disableDefaultPlugins ? [] : internalPlugins(flags)).map((plugin) =>
168
- Effect.tryPromise({
169
- try: () => plugin(input),
170
- catch: errorMessage,
171
- }).pipe(
172
- Effect.tapError((error) => Effect.logError("failed to load internal plugin", { name: plugin.name, error })),
173
- Effect.option,
166
+ if (!flags.fastBoot) {
167
+ const internalInits = yield* Effect.all(
168
+ (flags.disableDefaultPlugins ? [] : internalPlugins(flags)).map((plugin) =>
169
+ Effect.tryPromise({
170
+ try: () => plugin(input),
171
+ catch: errorMessage,
172
+ }).pipe(
173
+ Effect.tapError((error) => Effect.logError("failed to load internal plugin", { name: plugin.name, error })),
174
+ Effect.option,
175
+ ),
174
176
  ),
175
- ),
176
- { concurrency: 10 },
177
- )
178
- for (const init of internalInits) {
179
- if (init._tag === "Some") hooks.push(init.value)
180
- }
177
+ { concurrency: 10 },
178
+ )
179
+ for (const init of internalInits) {
180
+ if (init._tag === "Some") hooks.push(init.value)
181
+ }
182
+
183
+ const plugins = flags.pure ? [] : (cfg.plugin_origins ?? [])
184
+ if (plugins.length) yield* config.waitForDependencies()
185
+
186
+ const loaded = yield* Effect.promise(() =>
187
+ PluginLoader.loadExternal({
188
+ items: plugins,
189
+ kind: "server",
190
+ report: {
191
+ start(candidate) {},
192
+ missing(candidate, _retry, message) {},
193
+ error(candidate, _retry, stage, error, resolved) {
194
+ const spec = candidate.plan.spec
195
+ const cause = error instanceof Error ? (error.cause ?? error) : error
196
+ const message = stage === "load" ? errorMessage(error) : errorMessage(cause)
197
+
198
+ if (stage === "install") {
199
+ const parsed = parsePluginSpecifier(spec)
200
+ publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`)
201
+ return
202
+ }
203
+
204
+ if (stage === "compatibility") {
205
+ publishPluginError(`Plugin ${spec} skipped: ${message}`)
206
+ return
207
+ }
208
+
209
+ if (stage === "entry") {
210
+ publishPluginError(`Failed to load plugin ${spec}: ${message}`)
211
+ return
212
+ }
181
213
 
182
- const plugins = flags.pure ? [] : (cfg.plugin_origins ?? [])
183
- if (flags.pure && cfg.plugin_origins?.length) {
184
- }
185
- if (plugins.length) yield* config.waitForDependencies()
186
-
187
- const loaded = yield* Effect.promise(() =>
188
- PluginLoader.loadExternal({
189
- items: plugins,
190
- kind: "server",
191
- report: {
192
- start(candidate) {},
193
- missing(candidate, _retry, message) {},
194
- error(candidate, _retry, stage, error, resolved) {
195
- const spec = candidate.plan.spec
196
- const cause = error instanceof Error ? (error.cause ?? error) : error
197
- const message = stage === "load" ? errorMessage(error) : errorMessage(cause)
198
-
199
- if (stage === "install") {
200
- const parsed = parsePluginSpecifier(spec)
201
- publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`)
202
- return
203
- }
204
-
205
- if (stage === "compatibility") {
206
- publishPluginError(`Plugin ${spec} skipped: ${message}`)
207
- return
208
- }
209
-
210
- if (stage === "entry") {
211
214
  publishPluginError(`Failed to load plugin ${spec}: ${message}`)
212
- return
213
- }
214
-
215
- publishPluginError(`Failed to load plugin ${spec}: ${message}`)
215
+ },
216
216
  },
217
- },
218
- }),
219
- )
220
- for (const load of loaded) {
221
- if (!load) continue
222
-
223
- // Keep plugin execution sequential so hook registration and execution
224
- // order remains deterministic across plugin runs.
225
- yield* Effect.tryPromise({
226
- try: () => applyPlugin(load, input, hooks),
227
- catch: (err) => {
228
- const message = errorMessage(err)
229
- return message
230
- },
231
- }).pipe(
232
- Effect.tapError((error) => Effect.logError("failed to load plugin", { path: load.spec, error })),
233
- Effect.catch(() => {
234
- // TODO: make proper events for this
235
- // events.publish(Session.Event.Error, {
236
- // error: new NamedError.Unknown({
237
- // message: `Failed to load plugin ${load.spec}: ${message}`,
238
- // }).toObject(),
239
- // })
240
- return Effect.void
241
217
  }),
242
218
  )
219
+ for (const load of loaded) {
220
+ if (!load) continue
221
+
222
+ yield* Effect.tryPromise({
223
+ try: () => applyPlugin(load, input, hooks),
224
+ catch: (err) => {
225
+ const message = errorMessage(err)
226
+ return message
227
+ },
228
+ }).pipe(
229
+ Effect.tapError((error) => Effect.logError("failed to load plugin", { path: load.spec, error })),
230
+ Effect.catch(() => {
231
+ return Effect.void
232
+ }),
233
+ )
234
+ }
243
235
  }
244
236
 
245
237
  // Notify plugins of current config
@@ -47,6 +47,7 @@ import { InstanceState } from "@/effect/instance-state"
47
47
  import { TaskTool, type TaskPromptOps } from "@/tool/task"
48
48
  import { SessionRunState } from "./run-state"
49
49
  import { RuntimeFlags } from "@/effect/runtime-flags"
50
+ import { Memory } from "@/memory/memory"
50
51
  import { EventV2Bridge } from "@/event-v2-bridge"
51
52
  import { Database } from "@anymous-ai/core/database/database"
52
53
  import { ModelV2 } from "@anymous-ai/core/model"
@@ -139,6 +140,7 @@ const layer = Layer.effect(
139
140
  const llm = yield* LLM.Service
140
141
  const events = yield* EventV2Bridge.Service
141
142
  const flags = yield* RuntimeFlags.Service
143
+ const memory = yield* Memory.Service
142
144
  const database = yield* Database.Service
143
145
  const { db } = database
144
146
  const ops = Effect.fn("SessionPrompt.ops")(function* () {
@@ -1254,16 +1256,18 @@ const layer = Layer.effect(
1254
1256
 
1255
1257
  yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
1256
1258
 
1257
- const [skills, env, instructions, mcpInstructions, modelMsgs] = yield* Effect.all([
1259
+ const [skills, env, instructions, mcpInstructions, modelMsgs, sharedMemory] = yield* Effect.all([
1258
1260
  sys.skills(agent),
1259
1261
  sys.environment(model),
1260
1262
  instruction.system().pipe(Effect.orDie),
1261
1263
  sys.mcp(agent, session.permission),
1262
1264
  MessageV2.toModelMessagesEffect(msgs, model),
1265
+ memory.allText(),
1263
1266
  ])
1264
1267
  const system = [
1265
1268
  ...env,
1266
1269
  ...instructions,
1270
+ ...(sharedMemory ? [sharedMemory] : []),
1267
1271
  ...(mcpInstructions ? [mcpInstructions] : []),
1268
1272
  ...(skills ? [skills] : []),
1269
1273
  ]
@@ -1624,6 +1628,7 @@ export const node = LayerNode.make({
1624
1628
  LLM.node,
1625
1629
  EventV2Bridge.node,
1626
1630
  RuntimeFlags.node,
1631
+ Memory.node,
1627
1632
  Database.node,
1628
1633
  ],
1629
1634
  })
@@ -0,0 +1,417 @@
1
+ import { Effect, Schema, pipe } from "effect"
2
+ import * as Tool from "./tool"
3
+ import { spawn } from "child_process"
4
+ import DESCRIPTION from "./computer.txt"
5
+
6
+ const platform = process.platform
7
+
8
+ const poweshellScript = (script: string) =>
9
+ Effect.promise<string>(
10
+ () =>
11
+ new Promise((resolve, reject) => {
12
+ const proc = spawn("powershell", [
13
+ "-NoProfile",
14
+ "-NonInteractive",
15
+ "-Command",
16
+ script,
17
+ ])
18
+ let stdout = ""
19
+ let stderr = ""
20
+ proc.stdout.on("data", (d: Buffer) => (stdout += d.toString()))
21
+ proc.stderr.on("data", (d: Buffer) => (stderr += d.toString()))
22
+ proc.on("close", (code) => {
23
+ if (code === 0) resolve(stdout.trim())
24
+ else reject(new Error(stderr.trim() || `exit code ${code}`))
25
+ })
26
+ proc.on("error", reject)
27
+ }),
28
+ )
29
+
30
+ const captureScreenshot = Effect.fn("Computer.screenshot")(function* () {
31
+ if (platform === "win32") {
32
+ const base64 = yield* poweshellScript(`
33
+ Add-Type -AssemblyName System.Drawing
34
+ Add-Type -AssemblyName System.Windows.Forms
35
+ $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
36
+ $bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
37
+ $gfx = [System.Drawing.Graphics]::FromImage($bmp)
38
+ $gfx.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)
39
+ $ms = New-Object System.IO.MemoryStream
40
+ $bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
41
+ $bytes = $ms.ToArray()
42
+ [Convert]::ToBase64String($bytes)
43
+ $gfx.Dispose()
44
+ $bmp.Dispose()
45
+ $ms.Dispose()
46
+ `)
47
+ return `data:image/png;base64,${base64}`
48
+ }
49
+ if (platform === "darwin") {
50
+ const base64 = yield* Effect.promise<string>(
51
+ () =>
52
+ new Promise((resolve, reject) => {
53
+ const proc = spawn("screencapture", ["-x", "-t", "png", "-"])
54
+ const chunks: Buffer[] = []
55
+ proc.stdout.on("data", (d: Buffer) => chunks.push(d))
56
+ proc.on("close", (code) => {
57
+ if (code === 0) resolve(Buffer.concat(chunks).toString("base64"))
58
+ else reject(new Error(`screencapture exit code ${code}`))
59
+ })
60
+ proc.on("error", reject)
61
+ }),
62
+ )
63
+ return `data:image/png;base64,${base64}`
64
+ }
65
+ if (platform === "linux") {
66
+ const base64 = yield* Effect.promise<string>(
67
+ () =>
68
+ new Promise((resolve, reject) => {
69
+ const proc = spawn("import", ["-window", "root", "png:-"])
70
+ const chunks: Buffer[] = []
71
+ proc.stdout.on("data", (d: Buffer) => chunks.push(d))
72
+ proc.on("close", (code) => {
73
+ if (code === 0) resolve(Buffer.concat(chunks).toString("base64"))
74
+ else reject(new Error(`import exit code ${code}`))
75
+ })
76
+ proc.on("error", reject)
77
+ }),
78
+ )
79
+ return `data:image/png;base64,${base64}`
80
+ }
81
+ return yield* Effect.fail(new Error(`Unsupported platform: ${platform}`))
82
+ })
83
+
84
+ const moveMouse = Effect.fn("Computer.moveMouse")(function* (x: number, y: number) {
85
+ if (platform === "win32") {
86
+ yield* poweshellScript(`
87
+ Add-Type -AssemblyName System.Windows.Forms
88
+ [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(x)}, ${Math.round(y)})
89
+ `)
90
+ } else if (platform === "darwin") {
91
+ yield* Effect.promise(() =>
92
+ new Promise<void>((resolve, reject) => {
93
+ const proc = spawn("osascript", [
94
+ "-e",
95
+ `tell application "System Events" to set position of mouse to {${Math.round(x)}, ${Math.round(y)}}`,
96
+ ])
97
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
98
+ proc.on("error", reject)
99
+ }),
100
+ )
101
+ } else if (platform === "linux") {
102
+ yield* Effect.promise(() =>
103
+ new Promise<void>((resolve, reject) => {
104
+ const proc = spawn("xdotool", ["mousemove", String(Math.round(x)), String(Math.round(y))])
105
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
106
+ proc.on("error", reject)
107
+ }),
108
+ )
109
+ }
110
+ })
111
+
112
+ const clickMouse = Effect.fn("Computer.clickMouse")(function* (button: "left" | "right" | "middle") {
113
+ if (platform === "win32") {
114
+ const btn = button === "left" ? "[MouseButtons]::Left" : button === "right" ? "[MouseButtons]::Right" : "[MouseButtons]::Middle"
115
+ const down = button === "left" ? "0x2" : button === "right" ? "0x8" : "0x20"
116
+ const up = button === "left" ? "0x4" : button === "right" ? "0x10" : "0x40"
117
+ yield* poweshellScript(`
118
+ Add-Type -AssemblyName System.Windows.Forms
119
+ [System.Windows.Forms.Cursor]::Click()
120
+ `)
121
+ } else if (platform === "darwin") {
122
+ const btn = button === "left" ? "click" : button === "right" ? "click at (get position of mouse) using {button 2}" : "click at (get position of mouse) using {button 3}"
123
+ yield* Effect.promise(() =>
124
+ new Promise<void>((resolve, reject) => {
125
+ const proc = spawn("osascript", [
126
+ "-e",
127
+ `tell application "System Events" to ${btn}`,
128
+ ])
129
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
130
+ proc.on("error", reject)
131
+ }),
132
+ )
133
+ } else if (platform === "linux") {
134
+ const btn = button === "left" ? "1" : button === "right" ? "3" : "2"
135
+ yield* Effect.promise(() =>
136
+ new Promise<void>((resolve, reject) => {
137
+ const proc = spawn("xdotool", ["click", btn])
138
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
139
+ proc.on("error", reject)
140
+ }),
141
+ )
142
+ }
143
+ })
144
+
145
+ const typeText = Effect.fn("Computer.typeText")(function* (text: string) {
146
+ if (platform === "win32") {
147
+ const escaped = text.replace(/"/g, '`"').replace(/\$/g, "`$").replace(/\n/g, "`n").replace(/\r/g, "`r").replace(/\t/g, "`t")
148
+ yield* poweshellScript(`
149
+ Add-Type -AssemblyName System.Windows.Forms
150
+ [System.Windows.Forms.SendKeys]::SendWait("${escaped}")
151
+ `)
152
+ } else if (platform === "darwin") {
153
+ yield* Effect.promise(() =>
154
+ new Promise<void>((resolve, reject) => {
155
+ const proc = spawn("osascript", [
156
+ "-e",
157
+ `tell application "System Events" to keystroke "${text.replace(/"/g, '\\"')}"`,
158
+ ])
159
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
160
+ proc.on("error", reject)
161
+ }),
162
+ )
163
+ } else if (platform === "linux") {
164
+ yield* Effect.promise(() =>
165
+ new Promise<void>((resolve, reject) => {
166
+ const proc = spawn("xdotool", ["type", text])
167
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
168
+ proc.on("error", reject)
169
+ }),
170
+ )
171
+ }
172
+ })
173
+
174
+ const keyPress = Effect.fn("Computer.keyPress")(function* (keys: string[]) {
175
+ const combo = keys.join("+")
176
+ if (platform === "win32") {
177
+ const mapping: Record<string, string> = {
178
+ ctrl: "^", alt: "%", shift: "+", enter: "{ENTER}", tab: "{TAB}",
179
+ escape: "{ESC}", backspace: "{BACKSPACE}", delete: "{DELETE}",
180
+ up: "{UP}", down: "{DOWN}", left: "{LEFT}", right: "{RIGHT}",
181
+ home: "{HOME}", end: "{END}", pageup: "{PGUP}", pagedown: "{PGDN}",
182
+ }
183
+ const translated = keys.map((k) => mapping[k.toLowerCase()] ?? k.toUpperCase())
184
+ yield* poweshellScript(`
185
+ Add-Type -AssemblyName System.Windows.Forms
186
+ [System.Windows.Forms.SendKeys]::SendWait("${translated.join("")}")
187
+ `)
188
+ } else if (platform === "darwin") {
189
+ const mapping: Record<string, string> = {
190
+ ctrl: "command down", alt: "option down", shift: "shift down",
191
+ enter: "return", escape: "escape", tab: "tab",
192
+ backspace: "delete", delete: "forward delete",
193
+ up: "up", down: "down", left: "left", right: "right",
194
+ }
195
+ const parts = keys.map((k) => mapping[k.toLowerCase()] ?? `"${k}"`)
196
+ const cmd = `tell application "System Events" to key code ${keys.length > 0 ? `using {${parts.join(", ")}}` : ""}`
197
+ yield* Effect.promise(() =>
198
+ new Promise<void>((resolve, reject) => {
199
+ const proc = spawn("osascript", ["-e", cmd])
200
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
201
+ proc.on("error", reject)
202
+ }),
203
+ )
204
+ } else if (platform === "linux") {
205
+ yield* Effect.promise(() =>
206
+ new Promise<void>((resolve, reject) => {
207
+ const proc = spawn("xdotool", ["key", combo])
208
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
209
+ proc.on("error", reject)
210
+ }),
211
+ )
212
+ }
213
+ })
214
+
215
+ const scrollMouse = Effect.fn("Computer.scrollMouse")(function* (clicks: number) {
216
+ if (platform === "win32") {
217
+ yield* poweshellScript(`
218
+ Add-Type -AssemblyName System.Windows.Forms
219
+ [System.Windows.Forms.SendKeys]::SendWait("{${clicks > 0 ? "UP" : "DOWN"} ${Math.abs(clicks)}}")
220
+ `)
221
+ } else if (platform === "darwin") {
222
+ yield* Effect.promise(() =>
223
+ new Promise<void>((resolve, reject) => {
224
+ const proc = spawn("osascript", [
225
+ "-e",
226
+ `tell application "System Events" to scroll wheel ${clicks} lines`,
227
+ ])
228
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
229
+ proc.on("error", reject)
230
+ }),
231
+ )
232
+ } else if (platform === "linux") {
233
+ const btn = clicks > 0 ? "4" : "5"
234
+ const count = Math.abs(clicks)
235
+ yield* Effect.promise(() =>
236
+ new Promise<void>((resolve, reject) => {
237
+ const proc = spawn("xdotool", ["click", `--repeat`, String(count), btn])
238
+ proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))))
239
+ proc.on("error", reject)
240
+ }),
241
+ )
242
+ }
243
+ })
244
+
245
+ const getScreenSize = Effect.fn("Computer.screenSize")(function* () {
246
+ if (platform === "win32") {
247
+ const raw = yield* poweshellScript(`
248
+ Add-Type -AssemblyName System.Windows.Forms
249
+ $s = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
250
+ Write-Output "$($s.Width) $($s.Height)"
251
+ `)
252
+ const [w, h] = raw.split(" ").map(Number)
253
+ return { width: w, height: h }
254
+ }
255
+ if (platform === "darwin") {
256
+ const raw = yield* Effect.promise<string>(
257
+ () =>
258
+ new Promise((resolve, reject) => {
259
+ const proc = spawn("osascript", [
260
+ "-e",
261
+ `tell application "Finder" to get bounds of window of desktop`,
262
+ ])
263
+ let d = ""
264
+ proc.stdout.on("data", (b: Buffer) => (d += b.toString()))
265
+ proc.on("close", (code) => (code === 0 ? resolve(d.trim()) : reject(new Error(`exit ${code}`))))
266
+ proc.on("error", reject)
267
+ }),
268
+ )
269
+ const [_, __, w, h] = d.split(", ").map(Number)
270
+ return { width: w, height: h }
271
+ }
272
+ if (platform === "linux") {
273
+ const raw = yield* Effect.promise<string>(
274
+ () =>
275
+ new Promise((resolve, reject) => {
276
+ const proc = spawn("xdotool", ["getdisplaygeometry"])
277
+ let d = ""
278
+ proc.stdout.on("data", (b: Buffer) => (d += b.toString()))
279
+ proc.on("close", (code) => (code === 0 ? resolve(d.trim()) : reject(new Error(`exit ${code}`))))
280
+ proc.on("error", reject)
281
+ }),
282
+ )
283
+ const [w, h] = raw.split(/\s+/).map(Number)
284
+ return { width: w, height: h }
285
+ }
286
+ return yield* Effect.fail(new Error(`Unsupported platform: ${platform}`))
287
+ })
288
+
289
+ export const Action = Schema.Struct({
290
+ action: Schema.Literal("screenshot", "click", "doubleclick", "rightclick", "move", "type", "keypress", "scroll", "screensize"),
291
+ x: Schema.optional(Schema.Number).annotate({ description: "X coordinate for click/move actions" }),
292
+ y: Schema.optional(Schema.Number).annotate({ description: "Y coordinate for click/move actions" }),
293
+ text: Schema.optional(Schema.String).annotate({ description: "Text to type (for type action)" }),
294
+ keys: Schema.optional(Schema.Array(Schema.String)).annotate({ description: "Keys to press (for keypress action), e.g. ['ctrl', 'c']" }),
295
+ button: Schema.optional(Schema.Literal("left", "right", "middle")).annotate({ description: "Mouse button (default: left)" }),
296
+ clicks: Schema.optional(Schema.Number).annotate({ description: "Scroll clicks (positive=up, negative=down)" }),
297
+ })
298
+
299
+ export const ComputerTool = Tool.define<typeof Action, {}, Question.Service>(
300
+ "computer",
301
+ Effect.gen(function* () {
302
+ return {
303
+ description: DESCRIPTION,
304
+ parameters: Action,
305
+ execute: (params: Schema.Schema.Type<typeof Action>) =>
306
+ Effect.gen(function* () {
307
+ const { action } = params
308
+
309
+ if (action === "screenshot") {
310
+ const dataUrl = yield* captureScreenshot
311
+ return {
312
+ title: "Captured screenshot",
313
+ output: "Screenshot captured. Use the image to decide the next action.",
314
+ attachments: [{
315
+ type: "file",
316
+ mime: "image/png",
317
+ filename: "screenshot.png",
318
+ url: dataUrl,
319
+ }],
320
+ metadata: {},
321
+ }
322
+ }
323
+
324
+ if (action === "move") {
325
+ yield* moveMouse(params.x!, params.y!)
326
+ return { title: "Mouse moved", output: `Mouse moved to (${params.x}, ${params.y})`, metadata: {} }
327
+ }
328
+
329
+ if (action === "click" || action === "doubleclick" || action === "rightclick") {
330
+ if (params.x != null && params.y != null) {
331
+ yield* moveMouse(params.x, params.y)
332
+ }
333
+ const btn = action === "rightclick" ? "right" : (params.button ?? "left")
334
+ yield* clickMouse(btn as "left" | "right" | "middle")
335
+ if (action === "doubleclick") {
336
+ yield* clickMouse(btn as "left" | "right" | "middle")
337
+ }
338
+ const dataUrl = yield* captureScreenshot
339
+ return {
340
+ title: `${action} at (${params.x ?? "current"}, ${params.y ?? "current"})`,
341
+ output: `Performed ${action}. Screenshot shows the result.`,
342
+ attachments: [{
343
+ type: "file",
344
+ mime: "image/png",
345
+ filename: "screenshot.png",
346
+ url: dataUrl,
347
+ }],
348
+ metadata: {},
349
+ }
350
+ }
351
+
352
+ if (action === "type") {
353
+ yield* typeText(params.text!)
354
+ const dataUrl = yield* captureScreenshot
355
+ return {
356
+ title: `Typed text`,
357
+ output: `Typed "${params.text!.length > 50 ? params.text!.slice(0, 50) + "..." : params.text!}". Screenshot shows the result.`,
358
+ attachments: [{
359
+ type: "file",
360
+ mime: "image/png",
361
+ filename: "screenshot.png",
362
+ url: dataUrl,
363
+ }],
364
+ metadata: {},
365
+ }
366
+ }
367
+
368
+ if (action === "keypress") {
369
+ yield* keyPress(params.keys!)
370
+ const dataUrl = yield* captureScreenshot
371
+ return {
372
+ title: `Key press: ${params.keys!.join("+")}`,
373
+ output: `Pressed ${params.keys!.join("+")}. Screenshot shows the result.`,
374
+ attachments: [{
375
+ type: "file",
376
+ mime: "image/png",
377
+ filename: "screenshot.png",
378
+ url: dataUrl,
379
+ }],
380
+ metadata: {},
381
+ }
382
+ }
383
+
384
+ if (action === "scroll") {
385
+ yield* scrollMouse(params.clicks ?? 3)
386
+ const dataUrl = yield* captureScreenshot
387
+ return {
388
+ title: `Scrolled ${params.clicks ?? 3} clicks`,
389
+ output: `Scrolled. Screenshot shows the result.`,
390
+ attachments: [{
391
+ type: "file",
392
+ mime: "image/png",
393
+ filename: "screenshot.png",
394
+ url: dataUrl,
395
+ }],
396
+ metadata: {},
397
+ }
398
+ }
399
+
400
+ if (action === "screensize") {
401
+ const size = yield* getScreenSize
402
+ return {
403
+ title: "Screen size",
404
+ output: `Screen resolution: ${size.width}x${size.height}`,
405
+ metadata: {},
406
+ }
407
+ }
408
+
409
+ return {
410
+ title: "Unknown action",
411
+ output: `Unknown action: ${action}`,
412
+ metadata: {},
413
+ }
414
+ }).pipe(Effect.orDie),
415
+ }
416
+ }),
417
+ )
@@ -0,0 +1,16 @@
1
+ Control the computer screen — capture screenshots, move mouse, click, type text, press keys, and scroll. Returns a screenshot after most actions so you can see the result. Use this to navigate applications, interact with UIs, fill forms, click buttons, and control the desktop.
2
+
3
+ Available actions:
4
+ - screenshot: Capture current screen (returns image, no side effects)
5
+ - move (x, y): Move mouse to coordinates
6
+ - click (x?, y?, button?): Click at position (default: left button)
7
+ - doubleclick (x?, y?): Double-click at position
8
+ - rightclick (x?, y?): Right-click at position
9
+ - type (text): Type text at current cursor position
10
+ - keypress (keys): Press key combination (e.g. ["ctrl", "c"], ["alt", "tab"], ["enter"])
11
+ - scroll (clicks): Scroll up (positive) or down (negative)
12
+ - screensize: Get screen dimensions
13
+
14
+ Coordinate system: (0,0) is top-left corner of primary screen. Get screen dimensions via screensize action.
15
+
16
+ Platform support: Windows (native), macOS (via screencapture + osascript), Linux (via ImageMagick + xdotool).
@@ -0,0 +1,59 @@
1
+ import { Effect, Schema } from "effect"
2
+ import * as Tool from "./tool"
3
+ import { Memory } from "@/memory/memory"
4
+ import DESCRIPTION from "./memory.txt"
5
+
6
+ export const Parameters = Schema.Struct({
7
+ action: Schema.Literal("read", "write", "delete", "list").annotate({ description: "Action to perform" }),
8
+ key: Schema.optional(Schema.String).annotate({ description: "Memory key (required for read/write/delete)" }),
9
+ value: Schema.optional(Schema.String).annotate({ description: "Memory value (required for write)" }),
10
+ })
11
+
12
+ export const MemoryTool = Tool.define<typeof Parameters, {}, Memory.Service>(
13
+ "memory",
14
+ Effect.gen(function* () {
15
+ const memory = yield* Memory.Service
16
+
17
+ return {
18
+ description: DESCRIPTION,
19
+ parameters: Parameters,
20
+ execute: (params: Schema.Schema.Type<typeof Parameters>) =>
21
+ Effect.gen(function* () {
22
+ const { action, key, value } = params
23
+
24
+ if (action === "list") {
25
+ const entries = yield* memory.list()
26
+ if (entries.length === 0) {
27
+ return { title: "No memories", output: "No memories stored.", metadata: {} }
28
+ }
29
+ const formatted = entries.map((e) => `- ${e.key}: ${e.value}`).join("\n")
30
+ return { title: `Listed ${entries.length} memories`, output: formatted, metadata: {} }
31
+ }
32
+
33
+ if (action === "read") {
34
+ if (!key) return { title: "Error", output: "Key is required for read action.", metadata: {} }
35
+ const entry = yield* memory.read(key)
36
+ if (!entry) return { title: "Not found", output: `No memory found for key "${key}".`, metadata: {} }
37
+ return { title: `Read memory: ${key}`, output: entry.value, metadata: {} }
38
+ }
39
+
40
+ if (action === "write") {
41
+ if (!key || !value) {
42
+ return { title: "Error", output: "Both key and value are required for write action.", metadata: {} }
43
+ }
44
+ yield* memory.write(key, value)
45
+ return { title: `Memory saved: ${key}`, output: `Saved memory "${key}".`, metadata: {} }
46
+ }
47
+
48
+ if (action === "delete") {
49
+ if (!key) return { title: "Error", output: "Key is required for delete action.", metadata: {} }
50
+ const deleted = yield* memory.delete(key)
51
+ if (!deleted) return { title: "Not found", output: `No memory found for key "${key}".`, metadata: {} }
52
+ return { title: `Memory deleted: ${key}`, output: `Deleted memory "${key}".`, metadata: {} }
53
+ }
54
+
55
+ return { title: "Unknown action", output: `Unknown action: ${action}`, metadata: {} }
56
+ }).pipe(Effect.orDie),
57
+ }
58
+ }),
59
+ )