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 +1 -1
- package/src/cli/cmd/run/splash.ts +1 -1
- package/src/cli/ui.ts +2 -2
- package/src/memory/memory.ts +108 -0
- package/src/session/prompt.ts +6 -1
- package/src/tool/computer.ts +417 -0
- package/src/tool/computer.txt +16 -0
- package/src/tool/memory.ts +59 -0
- package/src/tool/memory.txt +14 -0
- package/src/tool/registry.ts +8 -0
package/package.json
CHANGED
|
@@ -171,7 +171,7 @@ function draw(
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
const VERSION = "1.
|
|
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.
|
|
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.
|
|
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"
|
package/src/session/prompt.ts
CHANGED
|
@@ -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
|
package/src/tool/registry.ts
CHANGED
|
@@ -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,
|