traw 0.2.1
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 +45 -0
- package/src/api/mo-client.ts +62 -0
- package/src/browser/controller.ts +196 -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,89 @@
|
|
|
1
|
+
name: Release Traw
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish-npm:
|
|
13
|
+
name: Publish to NPM
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Bun
|
|
20
|
+
uses: oven-sh/setup-bun@v2
|
|
21
|
+
with:
|
|
22
|
+
bun-version: latest
|
|
23
|
+
|
|
24
|
+
- name: Setup Node
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: '20'
|
|
28
|
+
registry-url: 'https://registry.npmjs.org'
|
|
29
|
+
|
|
30
|
+
- name: Get version
|
|
31
|
+
id: version
|
|
32
|
+
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
|
33
|
+
|
|
34
|
+
- name: Update package version
|
|
35
|
+
run: npm version ${{ steps.version.outputs.VERSION }} --no-git-tag-version --allow-same-version
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: bun install --frozen-lockfile --ignore-scripts
|
|
39
|
+
|
|
40
|
+
- name: Publish to NPM
|
|
41
|
+
run: npm publish --access public
|
|
42
|
+
env:
|
|
43
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
44
|
+
|
|
45
|
+
release:
|
|
46
|
+
name: Create Release
|
|
47
|
+
needs: [publish-npm]
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- name: Get version
|
|
51
|
+
id: version
|
|
52
|
+
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
|
53
|
+
|
|
54
|
+
- name: Create GitHub Release
|
|
55
|
+
uses: softprops/action-gh-release@v1
|
|
56
|
+
with:
|
|
57
|
+
name: Traw v${{ steps.version.outputs.VERSION }}
|
|
58
|
+
body: |
|
|
59
|
+
## Traw v${{ steps.version.outputs.VERSION }}
|
|
60
|
+
|
|
61
|
+
AI browser agent for terminal.
|
|
62
|
+
|
|
63
|
+
### Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# bun (recommended)
|
|
67
|
+
bun add -g traw
|
|
68
|
+
|
|
69
|
+
# npm
|
|
70
|
+
npm install -g traw
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Usage
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
traw auth
|
|
77
|
+
traw run "find weather in Moscow"
|
|
78
|
+
traw --help
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Check for updates
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
traw upd
|
|
85
|
+
```
|
|
86
|
+
draft: false
|
|
87
|
+
prerelease: false
|
|
88
|
+
env:
|
|
89
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
package/.gitmodules
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, zarazaex
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/assets/auth.gif
ADDED
|
Binary file
|
|
Binary file
|
package/assets/logo.png
ADDED
|
Binary file
|
package/bun.lock
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "traw",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"markdownly.js": "^1.1.0",
|
|
9
|
+
"playwright": "^1.52.0",
|
|
10
|
+
"zod": "^4.2.1",
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/bun": "latest",
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"packages": {
|
|
21
|
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
|
22
|
+
|
|
23
|
+
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
24
|
+
|
|
25
|
+
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
26
|
+
|
|
27
|
+
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
|
28
|
+
|
|
29
|
+
"markdownly.js": ["markdownly.js@1.1.0", "", { "bin": { "markdown": "dist/cli.js", "md": "dist/cli.js" } }, "sha512-OfSC5OlL7sbeExYRjJFNZzSpk8KQqZAQL9mbkrglqUOGWu9NEkIEPbqbgp+lnfcl8KgTeeAHvkpCRzNNFj0LhQ=="],
|
|
30
|
+
|
|
31
|
+
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
|
|
32
|
+
|
|
33
|
+
"playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
|
|
34
|
+
|
|
35
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
36
|
+
|
|
37
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
38
|
+
|
|
39
|
+
"zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "traw",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"module": "src/index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"traw": "./src/cli/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"traw": "bun run src/cli/index.ts",
|
|
11
|
+
"postinstall": "bunx playwright install firefox"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"markdownly.js": "^1.1.0",
|
|
15
|
+
"playwright": "^1.52.0",
|
|
16
|
+
"zod": "^4.2.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/bun": "latest"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"typescript": "^5"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="assets/logo.png" alt="Traw" width="300"/>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
<details>
|
|
12
|
+
<summary>🚨 Warning </summary>
|
|
13
|
+
This project uses reverse engineering api z.ai, do not use it for anything because of serious/personal data, it is just a toy - not a tool
|
|
14
|
+
<br/>
|
|
15
|
+
Plz Give Star
|
|
16
|
+
</details>
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## About
|
|
22
|
+
Traw is a simple and fast neuro agent that browses the internet instead of you
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
<img src="assets/expirience.png" alt="auth" style="border-radius: 9px;"/>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
## Fast Start
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# install the traw
|
|
33
|
+
git clone https://github.com/zarazaex69/traw
|
|
34
|
+
cd traw
|
|
35
|
+
bun install
|
|
36
|
+
|
|
37
|
+
# auth the traw
|
|
38
|
+
bun run traw auth
|
|
39
|
+
# mo server not found. install mo? [Y/n] Y
|
|
40
|
+
```
|
|
41
|
+
<div align="center">
|
|
42
|
+
<img src="assets/auth.gif" alt="auth" style="border-radius: 9px;"/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# start the traw
|
|
47
|
+
bun run traw run "your goal"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
<div align="center">
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Contact
|
|
55
|
+
|
|
56
|
+
Telegram: [zarazaex](https://t.me/zarazaexe)
|
|
57
|
+
<br>
|
|
58
|
+
Email: [zarazaex@tuta.io](mailto:zarazaex@tuta.io)
|
|
59
|
+
<br>
|
|
60
|
+
Site: [zarazaex.xyz](https://zarazaex.xyz)
|
|
61
|
+
<br>
|
|
62
|
+
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Action, AgentConfig, AgentStep, ChatMessage, PageState } from "../types"
|
|
2
|
+
import { BrowserController } from "../browser/controller"
|
|
3
|
+
import { MoClient } from "../api/mo-client"
|
|
4
|
+
import { log } from "../utils/log"
|
|
5
|
+
import { checkNotify, notify } from "../utils/notify"
|
|
6
|
+
import { systemPrompt, planningPrompt } from "./prompts"
|
|
7
|
+
|
|
8
|
+
export class Agent {
|
|
9
|
+
private browser: BrowserController
|
|
10
|
+
private mo: MoClient
|
|
11
|
+
private config: AgentConfig
|
|
12
|
+
private history: AgentStep[] = []
|
|
13
|
+
private messages: ChatMessage[] = []
|
|
14
|
+
private plan = ""
|
|
15
|
+
|
|
16
|
+
private aiTime = 0
|
|
17
|
+
private browserTime = 0
|
|
18
|
+
private startTime = 0
|
|
19
|
+
|
|
20
|
+
constructor(config: AgentConfig) {
|
|
21
|
+
this.config = config
|
|
22
|
+
this.browser = new BrowserController(config)
|
|
23
|
+
this.mo = new MoClient({
|
|
24
|
+
moUrl: config.moUrl,
|
|
25
|
+
apiUrl: config.apiUrl,
|
|
26
|
+
apiKey: config.apiKey,
|
|
27
|
+
model: config.model,
|
|
28
|
+
thinking: config.thinking,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async run(goal: string): Promise<{ history: AgentStep[]; video: string | null }> {
|
|
33
|
+
this.startTime = Date.now()
|
|
34
|
+
|
|
35
|
+
await checkNotify()
|
|
36
|
+
|
|
37
|
+
if (this.config.thinking) {
|
|
38
|
+
const planStart = Date.now()
|
|
39
|
+
log.planning()
|
|
40
|
+
this.plan = await this.createPlan(goal)
|
|
41
|
+
this.aiTime += Date.now() - planStart
|
|
42
|
+
log.planDone()
|
|
43
|
+
log.plan(this.plan)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log.openStart()
|
|
47
|
+
await this.browser.launch()
|
|
48
|
+
await this.browser.execute({
|
|
49
|
+
type: "goto",
|
|
50
|
+
text: "https://html.duckduckgo.com/html/",
|
|
51
|
+
reason: "start page",
|
|
52
|
+
})
|
|
53
|
+
log.openStop()
|
|
54
|
+
|
|
55
|
+
this.messages.push({ role: "system", content: systemPrompt })
|
|
56
|
+
|
|
57
|
+
const taskMessage = this.plan
|
|
58
|
+
? `Your task: ${goal}\n\nYour plan:\n${this.plan}\n\nYou have ${this.config.maxSteps} steps maximum. Be efficient. You are now on DuckDuckGo search.`
|
|
59
|
+
: `Your task: ${goal}\n\nYou have ${this.config.maxSteps} steps maximum. Be efficient. You are now on DuckDuckGo search.`
|
|
60
|
+
this.messages.push({ role: "user", content: taskMessage })
|
|
61
|
+
|
|
62
|
+
let finalReason = ""
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
for (let step = 0; step < this.config.maxSteps; step++) {
|
|
66
|
+
const loadStart = Date.now()
|
|
67
|
+
log.loadStart()
|
|
68
|
+
const state = await this.browser.getState()
|
|
69
|
+
log.loadStop()
|
|
70
|
+
this.browserTime += Date.now() - loadStart
|
|
71
|
+
|
|
72
|
+
log.step(step + 1, this.config.maxSteps, state.url)
|
|
73
|
+
|
|
74
|
+
const thinkStart = Date.now()
|
|
75
|
+
log.receiveStart()
|
|
76
|
+
const decision = await this.think(state)
|
|
77
|
+
log.receiveStop()
|
|
78
|
+
this.aiTime += Date.now() - thinkStart
|
|
79
|
+
|
|
80
|
+
log.thought(decision.thought)
|
|
81
|
+
const target = decision.action.index !== undefined
|
|
82
|
+
? `[${decision.action.index}] ${decision.action.text || ""}`
|
|
83
|
+
: decision.action.text
|
|
84
|
+
log.action(decision.action.type, target)
|
|
85
|
+
|
|
86
|
+
const execStart = Date.now()
|
|
87
|
+
const result = await this.browser.execute(decision.action)
|
|
88
|
+
this.browserTime += Date.now() - execStart
|
|
89
|
+
|
|
90
|
+
if (result.startsWith("error:")) {
|
|
91
|
+
log.fail(result)
|
|
92
|
+
} else {
|
|
93
|
+
log.ok()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.history.push({
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
thought: decision.thought,
|
|
99
|
+
action: decision.action,
|
|
100
|
+
result,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if (decision.action.type === "done") {
|
|
104
|
+
finalReason = decision.action.reason
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (decision.action.type === "back") {
|
|
109
|
+
step = Math.max(-1, step - 2)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await new Promise((r) => setTimeout(r, 300))
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
const videoPath = await this.browser.close()
|
|
116
|
+
const totalTime = Date.now() - this.startTime
|
|
117
|
+
log.done(this.history.length, finalReason)
|
|
118
|
+
log.stats(totalTime, this.aiTime, this.browserTime)
|
|
119
|
+
|
|
120
|
+
await notify("Agent done", `${this.history.length} steps completed`)
|
|
121
|
+
|
|
122
|
+
return { history: this.history, video: videoPath }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async createPlan(goal: string): Promise<string> {
|
|
127
|
+
return this.mo.chat([
|
|
128
|
+
{ role: "system", content: planningPrompt },
|
|
129
|
+
{ role: "user", content: `Goal: ${goal}\nMax steps available: ${this.config.maxSteps}` },
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private formatRecentHistory(): string {
|
|
134
|
+
if (this.history.length === 0) return ""
|
|
135
|
+
|
|
136
|
+
const lines = this.history.map((h, i) => {
|
|
137
|
+
return `${i + 1}. ${h.action.type}${h.action.index !== undefined ? ` [${h.action.index}]` : ""}${h.action.text ? ` "${h.action.text}"` : ""} → ${h.result}`
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return `\nPrevious actions:\n${lines.join("\n")}`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async think(state: PageState): Promise<{ thought: string; action: Action }> {
|
|
144
|
+
const historyBlock = this.formatRecentHistory()
|
|
145
|
+
|
|
146
|
+
const stateText = `URL: ${state.url}
|
|
147
|
+
Title: ${state.title}
|
|
148
|
+
${historyBlock}
|
|
149
|
+
|
|
150
|
+
Elements:
|
|
151
|
+
${state.text}
|
|
152
|
+
|
|
153
|
+
What's your next action?`
|
|
154
|
+
|
|
155
|
+
if (this.config.debug) {
|
|
156
|
+
console.log("\n" + state.text)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.messages.push({ role: "user", content: stateText })
|
|
160
|
+
|
|
161
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
162
|
+
const response = await this.mo.chat(this.messages)
|
|
163
|
+
const parsed = this.parseResponse(response)
|
|
164
|
+
|
|
165
|
+
if (parsed) {
|
|
166
|
+
this.messages.push({ role: "assistant", content: response })
|
|
167
|
+
return parsed
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (attempt < 2) {
|
|
171
|
+
log.fail(`parse error, retry ${attempt + 2}/3`)
|
|
172
|
+
this.messages.push({ role: "assistant", content: response })
|
|
173
|
+
this.messages.push({ role: "user", content: "Invalid JSON. Reply with valid JSON only, no markdown." })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
thought: "failed to parse response",
|
|
179
|
+
action: { type: "wait", reason: "parse error" },
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private parseResponse(response: string): { thought: string; action: Action } | null {
|
|
184
|
+
try {
|
|
185
|
+
let jsonStr = response
|
|
186
|
+
|
|
187
|
+
const match = response.match(/```(?:json)?\s*([\s\S]*?)```/)
|
|
188
|
+
if (match) {
|
|
189
|
+
jsonStr = match[1]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const parsed = JSON.parse(jsonStr.trim())
|
|
193
|
+
if (!parsed.action?.type) return null
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
thought: parsed.thought || "thinking...",
|
|
197
|
+
action: parsed.action,
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const logEntry = `\n--- ${new Date().toISOString()} ---\nError: ${err}\nResponse:\n${response}\n`
|
|
201
|
+
import("fs").then(fs => {
|
|
202
|
+
fs.appendFileSync("agent-errors.log", logEntry)
|
|
203
|
+
}).catch(() => {})
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const systemPrompt = `You a Traw -control a browser via DOM elements. Each element has an index [N].
|
|
2
|
+
|
|
3
|
+
LANGUAGE RULES (STRICT):
|
|
4
|
+
- "thought" field: ALWAYS in English, no exceptions
|
|
5
|
+
- "done" action "reason" field: ALWAYS in the SAME language as user's original query
|
|
6
|
+
- If user asked in Russian → answer in Russian
|
|
7
|
+
- If user asked in English → answer in English
|
|
8
|
+
- Match the user's language exactly
|
|
9
|
+
|
|
10
|
+
MARKDOWN FORMATTING (for "done" reason field):
|
|
11
|
+
The terminal supports rich markdown rendering. USE these features for better readability:
|
|
12
|
+
- **Headers**: Use ### for sections (e.g., ### Installation)
|
|
13
|
+
- **Bold**: Use **text** for emphasis
|
|
14
|
+
- **Italic**: Use *text* for subtle emphasis
|
|
15
|
+
- **Lists**: Use * or - for bullet points, 1. 2. 3. for numbered lists
|
|
16
|
+
- **Code**: Use \`inline code\` for commands, \`\`\`lang for code blocks
|
|
17
|
+
- **Links**: Use [text](url) format - they will be clickable
|
|
18
|
+
- **Blockquotes**: Use > for quotes or important notes
|
|
19
|
+
Structure your answers with headers and lists for easy scanning.
|
|
20
|
+
|
|
21
|
+
ACTIONS (use index to target elements):
|
|
22
|
+
- click: {"type":"click","index":N} - click element [N]
|
|
23
|
+
- type: {"type":"type","index":N,"text":"query"} - type into input [N]
|
|
24
|
+
- scroll: {"type":"scroll","direction":"down"} - scroll page
|
|
25
|
+
- goto: {"type":"goto","text":"url"} - navigate to URL
|
|
26
|
+
- wait: {"type":"wait"} - wait 2 seconds
|
|
27
|
+
- back: {"type":"back"} - go back to previous page (FREE action, gives +1 step back, use when current page is wrong/useless)
|
|
28
|
+
- done: {"type":"done","reason":"result"} - task complete, include answer IN USER'S LANGUAGE with markdown
|
|
29
|
+
|
|
30
|
+
OUTPUT (JSON only, no markdown wrapper):
|
|
31
|
+
{"thought":"English reasoning here","action":{"type":"click","index":0}}`
|
|
32
|
+
|
|
33
|
+
export const planningPrompt = `Create short numbered plan to accomplish goal via browser. Start from DuckDuckGo search.
|
|
34
|
+
|
|
35
|
+
IMPORTANT: Plan must fit within the given step limit. Each navigation/click/type = 1 step.
|
|
36
|
+
- If max steps is low (5-10): be very direct, skip optional steps
|
|
37
|
+
- If max steps is high (20+): can be more thorough
|
|
38
|
+
|
|
39
|
+
LANGUAGE RULES:
|
|
40
|
+
- Plan steps: ALWAYS write in English
|
|
41
|
+
- Final answer (when task is done): Write in the SAME language as user's query
|
|
42
|
+
|
|
43
|
+
MARKDOWN: You may use markdown in plan (headers, lists, bold) for better terminal rendering.
|
|
44
|
+
|
|
45
|
+
No JSON in plan.`
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ChatMessage } from "../types"
|
|
2
|
+
|
|
3
|
+
export interface MoClientOptions {
|
|
4
|
+
moUrl: string
|
|
5
|
+
apiUrl?: string
|
|
6
|
+
apiKey?: string
|
|
7
|
+
model: string
|
|
8
|
+
thinking: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class MoClient {
|
|
12
|
+
private url: string
|
|
13
|
+
private apiKey?: string
|
|
14
|
+
private model: string
|
|
15
|
+
private thinking: boolean
|
|
16
|
+
|
|
17
|
+
constructor(opts: MoClientOptions) {
|
|
18
|
+
// custom api takes priority over mo
|
|
19
|
+
this.url = opts.apiUrl || opts.moUrl
|
|
20
|
+
this.apiKey = opts.apiKey
|
|
21
|
+
this.model = opts.model
|
|
22
|
+
this.thinking = opts.thinking
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async chat(messages: ChatMessage[]): Promise<string> {
|
|
26
|
+
const headers: Record<string, string> = {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (this.apiKey) {
|
|
31
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resp = await fetch(`${this.url}/v1/chat/completions`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers,
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
model: this.model,
|
|
39
|
+
messages,
|
|
40
|
+
stream: false,
|
|
41
|
+
thinking: this.thinking,
|
|
42
|
+
}),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (!resp.ok) {
|
|
46
|
+
const body = await resp.text()
|
|
47
|
+
throw new Error(`mo error: ${resp.status} ${body}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = (await resp.json()) as {
|
|
51
|
+
choices: { message: { content: string }; finish_reason: string }[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// debug: check if response was truncated
|
|
55
|
+
const finish = data.choices[0]?.finish_reason
|
|
56
|
+
if (finish && finish !== "stop") {
|
|
57
|
+
console.warn(`[mo] finish_reason: ${finish} (response may be truncated)`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return data.choices[0]?.message?.content ?? ""
|
|
61
|
+
}
|
|
62
|
+
}
|