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.
@@ -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
@@ -0,0 +1,6 @@
1
+ [submodule "markdownly.js"]
2
+ path = markdownly.js
3
+ url = https://github.com/zarazaex69/markdownly.js.git
4
+ [submodule "mo"]
5
+ path = mo
6
+ url = https://github.com/zarazaex69/mo.git
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.
Binary file
Binary file
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
+ ![Bun](https://img.shields.io/badge/-Bun-0D1117?style=flat-square&logo=Bun&logoColor=F3E6D8)
8
+ ![License](https://img.shields.io/badge/license-BSD--3--Clause-0D1117?style=flat-square&logo=open-source-initiative&logoColor=green&labelColor=0D1117)
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
+ }