traw 0.2.2
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/.github/workflows/release.yml +89 -0
- package/.gitmodules +6 -0
- package/LICENSE +28 -0
- package/assets/auth.gif +0 -0
- package/assets/expirience.png +0 -0
- package/assets/logo.png +0 -0
- package/bun.lock +41 -0
- package/package.json +24 -0
- package/readme.md +63 -0
- package/src/agent/agent.ts +207 -0
- package/src/agent/prompts.ts +42 -0
- package/src/api/mo-client.ts +62 -0
- package/src/browser/controller.ts +188 -0
- package/src/cli/help.ts +37 -0
- package/src/cli/index.ts +281 -0
- package/src/index.ts +3 -0
- package/src/markdownly.d.ts +3 -0
- package/src/types.ts +47 -0
- package/src/utils/first-run.ts +27 -0
- package/src/utils/log.ts +222 -0
- package/src/utils/mo-manager.ts +153 -0
- package/src/utils/notify.ts +31 -0
- package/src/utils/version.ts +39 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { firefox, type Browser, type Page, type BrowserContext } from "playwright"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import type { Action, PageState, AgentConfig } from "../types"
|
|
4
|
+
|
|
5
|
+
export class BrowserController {
|
|
6
|
+
private browser: Browser | null = null
|
|
7
|
+
private context: BrowserContext | null = null
|
|
8
|
+
private page: Page | null = null
|
|
9
|
+
private config: AgentConfig
|
|
10
|
+
|
|
11
|
+
constructor(config: AgentConfig) {
|
|
12
|
+
this.config = config
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async launch(): Promise<void> {
|
|
16
|
+
this.browser = await firefox.launch({
|
|
17
|
+
headless: this.config.headless,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
this.context = await this.browser.newContext({
|
|
21
|
+
viewport: { width: 1280, height: 720 },
|
|
22
|
+
locale: "en-US",
|
|
23
|
+
timezoneId: "Europe/Moscow",
|
|
24
|
+
...(this.config.recordVideo && {
|
|
25
|
+
recordVideo: {
|
|
26
|
+
dir: "./traw-recordings",
|
|
27
|
+
size: { width: 1280, height: 720 },
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
})
|
|
31
|
+
this.page = await this.context.newPage()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async close(): Promise<string | null> {
|
|
35
|
+
let videoPath: string | null = null
|
|
36
|
+
|
|
37
|
+
if (this.page && this.config.recordVideo) {
|
|
38
|
+
try {
|
|
39
|
+
const video = this.page.video()
|
|
40
|
+
if (video) {
|
|
41
|
+
await this.page.close()
|
|
42
|
+
this.page = null
|
|
43
|
+
await mkdir("./traw-recordings", { recursive: true })
|
|
44
|
+
const savePath = `./traw-recordings/traw-${Date.now()}.webm`
|
|
45
|
+
await video.saveAs(savePath)
|
|
46
|
+
videoPath = savePath
|
|
47
|
+
}
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
console.error("[video error]", e.message)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.page) await this.page.close().catch(() => {})
|
|
54
|
+
if (this.context) await this.context.close().catch(() => {})
|
|
55
|
+
if (this.browser) await this.browser.close().catch(() => {})
|
|
56
|
+
|
|
57
|
+
return videoPath
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getState(): Promise<PageState> {
|
|
61
|
+
if (!this.page) throw new Error("browser not launched")
|
|
62
|
+
|
|
63
|
+
await this.page.waitForLoadState("domcontentloaded").catch(() => {})
|
|
64
|
+
|
|
65
|
+
const url = this.page.url()
|
|
66
|
+
const title = await this.page.title()
|
|
67
|
+
|
|
68
|
+
const xml = await this.page.evaluate(() => {
|
|
69
|
+
const esc = (s: string) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)
|
|
70
|
+
const out: string[] = ["<page>"]
|
|
71
|
+
let idx = 0
|
|
72
|
+
|
|
73
|
+
document.querySelectorAll("[data-idx]").forEach(el => el.removeAttribute("data-idx"))
|
|
74
|
+
|
|
75
|
+
const walk = (node: Element, depth: number) => {
|
|
76
|
+
const el = node as HTMLElement
|
|
77
|
+
if (el.offsetParent === null && el.tagName !== "BODY" && el.tagName !== "HTML") return
|
|
78
|
+
|
|
79
|
+
const tag = el.tagName.toLowerCase()
|
|
80
|
+
const indent = " ".repeat(depth)
|
|
81
|
+
|
|
82
|
+
const skipTags = ["script", "style", "noscript", "svg", "path", "meta", "link", "br", "hr"]
|
|
83
|
+
if (skipTags.includes(tag)) return
|
|
84
|
+
|
|
85
|
+
const interactiveTags = ["a", "button", "input", "textarea", "select"]
|
|
86
|
+
const hasRole = el.getAttribute("role")
|
|
87
|
+
const hasOnclick = el.hasAttribute("onclick")
|
|
88
|
+
const isInteractive = interactiveTags.includes(tag) || hasRole || hasOnclick
|
|
89
|
+
|
|
90
|
+
if (isInteractive) {
|
|
91
|
+
el.setAttribute("data-idx", String(idx))
|
|
92
|
+
|
|
93
|
+
const attrs: string[] = [`id="${idx}"`]
|
|
94
|
+
|
|
95
|
+
const type = (el as HTMLInputElement).type
|
|
96
|
+
if (type) attrs.push(`type="${type}"`)
|
|
97
|
+
|
|
98
|
+
const href = (el as HTMLAnchorElement).href
|
|
99
|
+
if (href && tag === "a") attrs.push(`href="${esc(href.slice(0, 80))}"`)
|
|
100
|
+
|
|
101
|
+
const val = (el as HTMLInputElement).value
|
|
102
|
+
if (val) attrs.push(`value="${esc(val.slice(0, 40))}"`)
|
|
103
|
+
|
|
104
|
+
if ((el as any).disabled) attrs.push(`disabled="true"`)
|
|
105
|
+
if ((el as any).checked) attrs.push(`checked="true"`)
|
|
106
|
+
if ((el as any).readOnly) attrs.push(`readonly="true"`)
|
|
107
|
+
if ((el as any).required) attrs.push(`required="true"`)
|
|
108
|
+
if (el.getAttribute("aria-expanded")) attrs.push(`expanded="${el.getAttribute("aria-expanded")}"`)
|
|
109
|
+
if (el.getAttribute("aria-selected") === "true") attrs.push(`selected="true"`)
|
|
110
|
+
|
|
111
|
+
const text = el.textContent?.trim().slice(0, 60) || ""
|
|
112
|
+
const ariaLabel = el.getAttribute("aria-label")
|
|
113
|
+
const placeholder = (el as HTMLInputElement).placeholder
|
|
114
|
+
const label = esc(ariaLabel || text || placeholder || "")
|
|
115
|
+
|
|
116
|
+
out.push(`${indent}<${tag} ${attrs.join(" ")}>${label}</${tag}>`)
|
|
117
|
+
idx++
|
|
118
|
+
} else {
|
|
119
|
+
const textTags = ["h1", "h2", "h3", "h4", "h5", "h6", "p", "li", "td", "th", "label"]
|
|
120
|
+
if (textTags.includes(tag)) {
|
|
121
|
+
const directText = Array.from(el.childNodes)
|
|
122
|
+
.filter(n => n.nodeType === 3)
|
|
123
|
+
.map(n => n.textContent?.trim())
|
|
124
|
+
.join(" ")
|
|
125
|
+
.trim()
|
|
126
|
+
.slice(0, 120)
|
|
127
|
+
|
|
128
|
+
if (directText.length > 2) {
|
|
129
|
+
out.push(`${indent}<${tag}>${esc(directText)}</${tag}>`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Array.from(el.children).forEach(child => walk(child, depth + 1))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
walk(document.body, 0)
|
|
138
|
+
out.push("</page>")
|
|
139
|
+
return out.join("\n")
|
|
140
|
+
}).catch(() => "<page></page>")
|
|
141
|
+
|
|
142
|
+
return { url, title, text: xml }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async execute(action: Action): Promise<string> {
|
|
146
|
+
if (!this.page) throw new Error("browser not launched")
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
switch (action.type) {
|
|
150
|
+
case "goto":
|
|
151
|
+
await this.page.goto(action.text!, { waitUntil: "domcontentloaded", timeout: 15000 })
|
|
152
|
+
return `navigated to ${action.text}`
|
|
153
|
+
|
|
154
|
+
case "click":
|
|
155
|
+
const clickEl = this.page.locator(`[data-idx="${action.index}"]`)
|
|
156
|
+
await clickEl.click({ timeout: 5000 })
|
|
157
|
+
return `clicked [${action.index}]`
|
|
158
|
+
|
|
159
|
+
case "type":
|
|
160
|
+
const typeEl = this.page.locator(`[data-idx="${action.index}"]`)
|
|
161
|
+
await typeEl.fill(action.text!)
|
|
162
|
+
await this.page.waitForTimeout(300)
|
|
163
|
+
return `typed "${action.text}" into [${action.index}]`
|
|
164
|
+
|
|
165
|
+
case "scroll":
|
|
166
|
+
const delta = action.direction === "down" ? 500 : -500
|
|
167
|
+
await this.page.mouse.wheel(0, delta)
|
|
168
|
+
return `scrolled ${action.direction}`
|
|
169
|
+
|
|
170
|
+
case "wait":
|
|
171
|
+
await this.page.waitForTimeout(2000)
|
|
172
|
+
return "waited 2s"
|
|
173
|
+
|
|
174
|
+
case "back":
|
|
175
|
+
await this.page.goBack({ waitUntil: "domcontentloaded", timeout: 10000 })
|
|
176
|
+
return "went back"
|
|
177
|
+
|
|
178
|
+
case "done":
|
|
179
|
+
return "done"
|
|
180
|
+
|
|
181
|
+
default:
|
|
182
|
+
return `unknown action: ${action.type}`
|
|
183
|
+
}
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
return `error: ${err.message}`
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/cli/help.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { VERSION } from "../utils/version"
|
|
2
|
+
|
|
3
|
+
export function printHelp() {
|
|
4
|
+
console.log(`
|
|
5
|
+
traw v${VERSION} - AI browser agent
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
traw run "your goal here"
|
|
9
|
+
traw auth
|
|
10
|
+
traw upd
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
run execute browser agent with a goal
|
|
14
|
+
auth register new account and set as default
|
|
15
|
+
upd check for updates
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--fast use fast model (glm-4-flash, no thinking)
|
|
19
|
+
--headless run without visible browser (default)
|
|
20
|
+
--headed show browser window
|
|
21
|
+
--video enable video recording
|
|
22
|
+
--vision send screenshots to AI (visual mode)
|
|
23
|
+
--steps=N max steps (default: 20)
|
|
24
|
+
--mo=URL mo server url (default: http://localhost:8804)
|
|
25
|
+
--api=URL custom OpenAI-compatible API url (bypasses mo)
|
|
26
|
+
--api-key=KEY API key for custom endpoint (or use OPENAI_API_KEY env)
|
|
27
|
+
--model=NAME model name (default: glm-4.7)
|
|
28
|
+
-v, --version show version
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
traw auth
|
|
32
|
+
traw run "find the weather in Moscow"
|
|
33
|
+
traw run --fast "quick search for bun.js"
|
|
34
|
+
traw run --video "search for documentation"
|
|
35
|
+
traw run --api=https://api.openai.com --model=gpt-4o "search for news"
|
|
36
|
+
`)
|
|
37
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Agent } from "../agent/agent"
|
|
3
|
+
import type { AgentConfig } from "../types"
|
|
4
|
+
import { log, setSilent } from "../utils/log"
|
|
5
|
+
import { pingMo, isMoInstalled, downloadMo, startMo, stopMo } from "../utils/mo-manager"
|
|
6
|
+
import { printHelp } from "./help"
|
|
7
|
+
import { VERSION, checkForUpdates } from "../utils/version"
|
|
8
|
+
import { checkFirstRun, markFirstRunDone, showStarBanner } from "../utils/first-run"
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MO_PORT = 8804
|
|
11
|
+
|
|
12
|
+
const defaultConfig: AgentConfig = {
|
|
13
|
+
moUrl: `http://localhost:${DEFAULT_MO_PORT}`,
|
|
14
|
+
apiUrl: undefined,
|
|
15
|
+
apiKey: process.env.OPENAI_API_KEY || process.env.API_KEY,
|
|
16
|
+
model: "glm-4.7",
|
|
17
|
+
thinking: true,
|
|
18
|
+
headless: true,
|
|
19
|
+
recordVideo: false,
|
|
20
|
+
maxSteps: 20,
|
|
21
|
+
debug: false,
|
|
22
|
+
jsonOutput: false,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function prompt(question: string): Promise<string> {
|
|
26
|
+
process.stdout.write(question)
|
|
27
|
+
for await (const line of console) {
|
|
28
|
+
return line.trim()
|
|
29
|
+
}
|
|
30
|
+
return ""
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureMo(moUrl: string): Promise<boolean> {
|
|
34
|
+
// check if mo is already running
|
|
35
|
+
if (await pingMo(moUrl)) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// mo not running, check if installed
|
|
40
|
+
const installed = await isMoInstalled()
|
|
41
|
+
|
|
42
|
+
if (!installed) {
|
|
43
|
+
const answer = await prompt("mo server not found. install mo? [Y/n] ")
|
|
44
|
+
if (answer.toLowerCase() === "n") {
|
|
45
|
+
log.error("mo is required to run traw")
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await downloadMo()
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
log.error(`failed to install mo: ${err.message}`)
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// start mo
|
|
58
|
+
try {
|
|
59
|
+
const port = parseInt(new URL(moUrl).port) || DEFAULT_MO_PORT
|
|
60
|
+
await startMo(port)
|
|
61
|
+
return true
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
log.error(`failed to start mo: ${err.message}`)
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function registerAccount(moUrl: string): Promise<void> {
|
|
69
|
+
log.info("registering new account...")
|
|
70
|
+
log.info("browser will open for captcha solving")
|
|
71
|
+
|
|
72
|
+
const resp = await fetch(`${moUrl}/auth/register`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (!resp.ok) {
|
|
78
|
+
const err = await resp.json().catch(() => ({ error: resp.statusText }))
|
|
79
|
+
throw new Error(err.error || `registration failed: ${resp.status}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = await resp.json() as {
|
|
83
|
+
success: boolean
|
|
84
|
+
token: { id: string; email: string; active: boolean }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!data.success) {
|
|
88
|
+
throw new Error("registration failed")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
log.success(`registered: ${data.token.email}`)
|
|
92
|
+
log.info(`token id: ${data.token.id}`)
|
|
93
|
+
log.info("token is now active and ready to use")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
const args = process.argv.slice(2)
|
|
98
|
+
|
|
99
|
+
// show star banner on first run (non-blocking)
|
|
100
|
+
if (await checkFirstRun()) {
|
|
101
|
+
showStarBanner()
|
|
102
|
+
await markFirstRunDone()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
106
|
+
printHelp()
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cmd = args[0]
|
|
111
|
+
|
|
112
|
+
// handle version command
|
|
113
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
114
|
+
console.log(`traw ${VERSION}`)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// handle update check command
|
|
119
|
+
if (cmd === "upd" || cmd === "update") {
|
|
120
|
+
log.info(`current version: ${VERSION}`)
|
|
121
|
+
log.info("checking for updates...")
|
|
122
|
+
|
|
123
|
+
const update = await checkForUpdates()
|
|
124
|
+
if (!update) {
|
|
125
|
+
log.error("failed to check for updates")
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (update.hasUpdate) {
|
|
130
|
+
console.log()
|
|
131
|
+
log.success(`new version available: ${update.latest}`)
|
|
132
|
+
log.info(`download: ${update.url}`)
|
|
133
|
+
} else {
|
|
134
|
+
log.success("you're on the latest version!")
|
|
135
|
+
}
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// handle auth command
|
|
140
|
+
if (cmd === "auth") {
|
|
141
|
+
let moUrl = defaultConfig.moUrl
|
|
142
|
+
for (let i = 1; i < args.length; i++) {
|
|
143
|
+
if (args[i].startsWith("--mo=")) {
|
|
144
|
+
moUrl = args[i].split("=")[1]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!await ensureMo(moUrl)) {
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await registerAccount(moUrl)
|
|
154
|
+
} catch (err: any) {
|
|
155
|
+
log.error(err.message)
|
|
156
|
+
process.exit(1)
|
|
157
|
+
} finally {
|
|
158
|
+
stopMo()
|
|
159
|
+
}
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (cmd !== "run") {
|
|
164
|
+
console.error(`unknown command: ${cmd}`)
|
|
165
|
+
process.exit(1)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const config = { ...defaultConfig }
|
|
169
|
+
const goalParts: string[] = []
|
|
170
|
+
|
|
171
|
+
for (let i = 1; i < args.length; i++) {
|
|
172
|
+
const arg = args[i]
|
|
173
|
+
|
|
174
|
+
if (arg === "--headless") {
|
|
175
|
+
config.headless = true
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
if (arg === "--no-headless" || arg === "--headed") {
|
|
179
|
+
config.headless = false
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
if (arg === "--video") {
|
|
183
|
+
config.recordVideo = true
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
if (arg === "--fast") {
|
|
187
|
+
config.model = "0727-106B-API"
|
|
188
|
+
config.thinking = false
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
if (arg === "--debug") {
|
|
192
|
+
config.debug = true
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if (arg === "--json") {
|
|
196
|
+
config.jsonOutput = true
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
if (arg.startsWith("--steps=")) {
|
|
200
|
+
config.maxSteps = parseInt(arg.split("=")[1])
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
if (arg.startsWith("--mo=")) {
|
|
204
|
+
config.moUrl = arg.split("=")[1]
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
if (arg.startsWith("--api=")) {
|
|
208
|
+
config.apiUrl = arg.split("=")[1]
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if (arg.startsWith("--api-key=")) {
|
|
212
|
+
config.apiKey = arg.split("=")[1]
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
if (arg.startsWith("--model=")) {
|
|
216
|
+
config.model = arg.split("=")[1]
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
if (!arg.startsWith("--")) {
|
|
220
|
+
goalParts.push(arg)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const goal = goalParts.join(" ")
|
|
225
|
+
if (!goal) {
|
|
226
|
+
log.error("provide a goal: bun run traw run \"your goal\"")
|
|
227
|
+
process.exit(1)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!config.jsonOutput) {
|
|
231
|
+
log.header(goal)
|
|
232
|
+
log.config({
|
|
233
|
+
api: config.apiUrl || config.moUrl,
|
|
234
|
+
model: config.model,
|
|
235
|
+
headless: config.headless,
|
|
236
|
+
video: config.recordVideo,
|
|
237
|
+
steps: config.maxSteps,
|
|
238
|
+
})
|
|
239
|
+
} else {
|
|
240
|
+
setSilent(true)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// skip mo setup if using custom api
|
|
244
|
+
if (!config.apiUrl && !await ensureMo(config.moUrl)) {
|
|
245
|
+
process.exit(1)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const agent = new Agent(config)
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const result = await agent.run(goal)
|
|
252
|
+
|
|
253
|
+
if (config.jsonOutput) {
|
|
254
|
+
const output = {
|
|
255
|
+
success: true,
|
|
256
|
+
goal,
|
|
257
|
+
steps: result.history,
|
|
258
|
+
video: result.video,
|
|
259
|
+
}
|
|
260
|
+
console.log(JSON.stringify(output))
|
|
261
|
+
} else if (result.video) {
|
|
262
|
+
log.video(result.video)
|
|
263
|
+
}
|
|
264
|
+
} catch (err: any) {
|
|
265
|
+
if (config.jsonOutput) {
|
|
266
|
+
const output = {
|
|
267
|
+
success: false,
|
|
268
|
+
goal,
|
|
269
|
+
error: err.message,
|
|
270
|
+
}
|
|
271
|
+
console.log(JSON.stringify(output))
|
|
272
|
+
process.exit(1)
|
|
273
|
+
}
|
|
274
|
+
log.error(err.message)
|
|
275
|
+
process.exit(1)
|
|
276
|
+
} finally {
|
|
277
|
+
stopMo()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
main()
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type ActionType =
|
|
2
|
+
| "click"
|
|
3
|
+
| "type"
|
|
4
|
+
| "scroll"
|
|
5
|
+
| "goto"
|
|
6
|
+
| "wait"
|
|
7
|
+
| "back"
|
|
8
|
+
| "done"
|
|
9
|
+
|
|
10
|
+
export interface Action {
|
|
11
|
+
type: ActionType
|
|
12
|
+
index?: number
|
|
13
|
+
text?: string
|
|
14
|
+
direction?: "up" | "down"
|
|
15
|
+
reason: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PageState {
|
|
19
|
+
url: string
|
|
20
|
+
title: string
|
|
21
|
+
text: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AgentStep {
|
|
25
|
+
timestamp: number
|
|
26
|
+
thought: string
|
|
27
|
+
action: Action
|
|
28
|
+
result?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentConfig {
|
|
32
|
+
moUrl: string
|
|
33
|
+
apiUrl?: string
|
|
34
|
+
apiKey?: string
|
|
35
|
+
model: string
|
|
36
|
+
thinking: boolean
|
|
37
|
+
headless: boolean
|
|
38
|
+
recordVideo: boolean
|
|
39
|
+
maxSteps: number
|
|
40
|
+
debug: boolean
|
|
41
|
+
jsonOutput: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ChatMessage {
|
|
45
|
+
role: "system" | "user" | "assistant"
|
|
46
|
+
content: string
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { homedir } from "os"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".traw")
|
|
5
|
+
const FIRST_RUN_FILE = join(CONFIG_DIR, ".first-run-done")
|
|
6
|
+
|
|
7
|
+
const c = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
yellow: "\x1b[33m",
|
|
10
|
+
cyan: "\x1b[36m",
|
|
11
|
+
dim: "\x1b[2m",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function checkFirstRun(): Promise<boolean> {
|
|
15
|
+
const file = Bun.file(FIRST_RUN_FILE)
|
|
16
|
+
return !(await file.exists())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function markFirstRunDone(): Promise<void> {
|
|
20
|
+
await Bun.write(FIRST_RUN_FILE, "1")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function showStarBanner(): void {
|
|
24
|
+
console.log()
|
|
25
|
+
console.log(`${c.yellow}*${c.reset} like traw? give it a star: ${c.cyan}https://github.com/zarazaex69/traw${c.reset}`)
|
|
26
|
+
console.log()
|
|
27
|
+
}
|