anymous 1.1.6 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anymous",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "AI-powered reverse engineering platform",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -171,7 +171,7 @@ function draw(
171
171
  }
172
172
  }
173
173
 
174
- const VERSION = "1.1.6"
174
+ const VERSION = "1.2.0"
175
175
 
176
176
  function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: ScrollbackRenderContext): ScrollbackSnapshot {
177
177
  const width = Math.max(1, ctx.width)
package/src/cli/ui.ts CHANGED
@@ -54,7 +54,7 @@ export function logo(pad?: string) {
54
54
  result.push(row)
55
55
  result.push(EOL)
56
56
  }
57
- result.push("AI-Powered Reverse Engineering & Pentest Platform v1.1.6")
57
+ result.push("AI-Powered Reverse Engineering & Pentest Platform v1.2.0")
58
58
  return result.join("")
59
59
  }
60
60
 
@@ -103,7 +103,7 @@ export function logo(pad?: string) {
103
103
  result.push(EOL)
104
104
  })
105
105
  result.push(Style.TEXT_NORMAL, "─".repeat(45), EOL)
106
- result.push(Style.TEXT_INFO, "▸", Style.TEXT_NORMAL, " AI-Powered Reverse Engineering & Pentest Platform ", Style.TEXT_DIM, "v1.1.6", EOL)
106
+ result.push(Style.TEXT_INFO, "▸", Style.TEXT_NORMAL, " AI-Powered Reverse Engineering & Pentest Platform ", Style.TEXT_DIM, "v1.2.0", EOL)
107
107
  result.push(Style.TEXT_NORMAL, "─".repeat(45))
108
108
  return result.join("").trimEnd()
109
109
  }
@@ -0,0 +1,108 @@
1
+ import { LayerNode } from "@anymous-ai/core/effect/layer-node"
2
+ import { Effect, Layer, Context, Schema, ParseResult } from "effect"
3
+ import { FSUtil } from "@anymous-ai/core/fs-util"
4
+ import { Global } from "@anymous-ai/core/global"
5
+ import path from "path"
6
+
7
+ const MemoryEntry = Schema.Struct({
8
+ key: Schema.String,
9
+ value: Schema.String,
10
+ timestamp: Schema.Number,
11
+ sessionID: Schema.optional(Schema.String),
12
+ })
13
+
14
+ const MemoryStore = Schema.Struct({
15
+ version: Schema.Literal(1),
16
+ memories: Schema.Array(MemoryEntry),
17
+ })
18
+
19
+ interface MemoryEntryType extends Schema.Schema.Type<typeof MemoryEntry> {}
20
+ interface MemoryStoreType extends Schema.Schema.Type<typeof MemoryStore> {}
21
+
22
+ export interface Interface {
23
+ readonly read: (key: string) => Effect.Effect<MemoryEntryType | undefined>
24
+ readonly write: (key: string, value: string, sessionID?: string) => Effect.Effect<void>
25
+ readonly list: () => Effect.Effect<MemoryEntryType[]>
26
+ readonly delete: (key: string) => Effect.Effect<boolean>
27
+ readonly allText: () => Effect.Effect<string | undefined>
28
+ }
29
+
30
+ export class Service extends Context.Service<Service, Interface>()("@anymous/Memory") {}
31
+
32
+ const memoryFilePath = (global: { config: string }) => path.join(global.config, "memory.json")
33
+
34
+ const loadStore = (fs: FSUtil.Interface, filePath: string) =>
35
+ Effect.gen(function* () {
36
+ const exists = yield* fs.exists(filePath)
37
+ if (!exists) {
38
+ return { version: 1, memories: [] } as MemoryStoreType
39
+ }
40
+ const raw = yield* fs.readFileString(filePath)
41
+ const parsed = JSON.parse(raw) as unknown
42
+ const decoded = yield* Schema.decodeUnknown(MemoryStore)(parsed).pipe(
43
+ Effect.catchAll(() => Effect.succeed({ version: 1, memories: [] } as MemoryStoreType)),
44
+ )
45
+ return decoded
46
+ })
47
+
48
+ const saveStore = (fs: FSUtil.Interface, filePath: string, store: MemoryStoreType) =>
49
+ Effect.gen(function* () {
50
+ yield* fs.writeFileString(filePath, JSON.stringify(store, null, 2))
51
+ })
52
+
53
+ const layer = Layer.effect(
54
+ Service,
55
+ Effect.gen(function* () {
56
+ const fs = yield* FSUtil.Service
57
+ const global = yield* Global.Service
58
+ const filePath = memoryFilePath(global)
59
+
60
+ const read: Interface["read"] = Effect.fn("Memory.read")(function* (key: string) {
61
+ const store = yield* loadStore(fs, filePath)
62
+ return store.memories.find((m) => m.key === key)
63
+ })
64
+
65
+ const write: Interface["write"] = Effect.fn("Memory.write")(function* (key: string, value: string, sessionID?: string) {
66
+ const store = yield* loadStore(fs, filePath)
67
+ const existing = store.memories.findIndex((m) => m.key === key)
68
+ const entry: MemoryEntryType = { key, value, timestamp: Date.now(), ...(sessionID ? { sessionID } : {}) }
69
+ if (existing >= 0) {
70
+ store.memories[existing] = entry
71
+ } else {
72
+ store.memories.push(entry)
73
+ }
74
+ yield* saveStore(fs, filePath, store)
75
+ })
76
+
77
+ const list: Interface["list"] = Effect.fn("Memory.list")(function* () {
78
+ const store = yield* loadStore(fs, filePath)
79
+ return store.memories
80
+ })
81
+
82
+ const remove: Interface["delete"] = Effect.fn("Memory.delete")(function* (key: string) {
83
+ const store = yield* loadStore(fs, filePath)
84
+ const idx = store.memories.findIndex((m) => m.key === key)
85
+ if (idx < 0) return false
86
+ store.memories.splice(idx, 1)
87
+ yield* saveStore(fs, filePath, store)
88
+ return true
89
+ })
90
+
91
+ const allText: Interface["allText"] = Effect.fn("Memory.allText")(function* () {
92
+ const store = yield* loadStore(fs, filePath)
93
+ if (store.memories.length === 0) return undefined
94
+ const lines = store.memories.map((m) => `- ${m.key}: ${m.value}`)
95
+ return `## Shared Context / Memory\n\n${lines.join("\n")}`
96
+ })
97
+
98
+ return Service.of({ read, write, list, delete: remove, allText })
99
+ }),
100
+ )
101
+
102
+ export const node = LayerNode.make({
103
+ service: Service,
104
+ layer: layer.pipe(Layer.orDie),
105
+ deps: [FSUtil.node, Global.node],
106
+ })
107
+
108
+ export * as Memory from "./memory"
@@ -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
+ )
@@ -0,0 +1,14 @@
1
+ Persistent shared context memory that persists across sessions. Use this to remember important information about the project, user preferences, architecture decisions, credentials, or any context you want to carry between sessions.
2
+
3
+ Actions:
4
+ - read (key): Read a specific memory
5
+ - write (key, value): Save or update a memory
6
+ - delete (key): Remove a memory
7
+ - list: List all memories
8
+
9
+ Memories are automatically injected into the system prompt at the start of every session as shared context. Use this for:
10
+ - Project architecture decisions
11
+ - Coding conventions and preferences
12
+ - Authentication tokens or API keys
13
+ - Important findings or TODOs
14
+ - User preferences for how you should behave
@@ -2,6 +2,8 @@ import { LayerNode } from "@anymous-ai/core/effect/layer-node"
2
2
  import { httpClient } from "@anymous-ai/core/effect/app-node-platform"
3
3
  import { Ripgrep } from "@anymous-ai/core/ripgrep"
4
4
  import { PlanExitTool } from "./plan"
5
+ import { ComputerTool } from "./computer"
6
+ import { MemoryTool } from "./memory"
5
7
  import { Session } from "@/session/session"
6
8
  import { QuestionTool } from "./question"
7
9
  import { ShellTool } from "./shell"
@@ -100,6 +102,8 @@ const layer = Layer.effect(
100
102
  const todo = yield* TodoWriteTool
101
103
  const lsptool = yield* LspTool
102
104
  const plan = yield* PlanExitTool
105
+ const computer = yield* ComputerTool
106
+ const memorytool = yield* MemoryTool
103
107
  const webfetch = yield* WebFetchTool
104
108
  const websearch = yield* WebSearchTool
105
109
  const shell = yield* ShellTool
@@ -218,6 +222,8 @@ const layer = Layer.effect(
218
222
  question: Tool.init(question),
219
223
  lsp: Tool.init(lsptool),
220
224
  plan: Tool.init(plan),
225
+ computer: Tool.init(computer),
226
+ memory: Tool.init(memorytool),
221
227
  ...(codeModeTool ? { execute: Tool.init(codeModeTool) } : {}),
222
228
  })
223
229
 
@@ -240,6 +246,8 @@ const layer = Layer.effect(
240
246
  tool.patch,
241
247
  ...(tool.execute ? [tool.execute] : []),
242
248
  ...(flags.experimentalLspTool ? [tool.lsp] : []),
249
+ tool.computer,
250
+ tool.memory,
243
251
  ...(flags.experimentalPlanMode && flags.client === "cli" ? [tool.plan] : []),
244
252
  ],
245
253
  task: tool.task,