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.
@@ -0,0 +1,222 @@
1
+ import { markdown } from "markdownly.js"
2
+
3
+ let silent = false
4
+
5
+ export function setSilent(value: boolean) {
6
+ silent = value
7
+ }
8
+
9
+ function renderMd(text: string): string {
10
+ try {
11
+ return markdown(text).trim()
12
+ } catch {
13
+ return text
14
+ }
15
+ }
16
+
17
+ const c = {
18
+ reset: "\x1b[0m",
19
+ dim: "\x1b[2m",
20
+ bold: "\x1b[1m",
21
+ cyan: "\x1b[36m",
22
+ green: "\x1b[32m",
23
+ yellow: "\x1b[33m",
24
+ red: "\x1b[31m",
25
+ magenta: "\x1b[35m",
26
+ blue: "\x1b[34m",
27
+ gray: "\x1b[90m",
28
+ }
29
+
30
+ const icons = {
31
+ arrow: "→",
32
+ check: "✓",
33
+ cross: "✗",
34
+ dot: "(0)",
35
+ circle: "○",
36
+ brain: "!",
37
+ play: ">",
38
+ }
39
+
40
+ class Spinner {
41
+ private frames = [".", "..", "..."]
42
+ private idx = 0
43
+ private interval: ReturnType<typeof setInterval> | null = null
44
+ private label: string
45
+
46
+ constructor(label: string) {
47
+ this.label = label
48
+ }
49
+
50
+ start() {
51
+ process.stdout.write("\x1b[?25l")
52
+ process.stdout.write(` ${c.dim}${this.label}...${c.reset}`)
53
+ this.interval = setInterval(() => {
54
+ this.idx = (this.idx + 1) % this.frames.length
55
+ process.stdout.write(`\r\x1b[K ${c.dim}${this.label}${this.frames[this.idx]}${c.reset}`)
56
+ }, 250)
57
+ }
58
+
59
+ stop() {
60
+ if (this.interval) {
61
+ clearInterval(this.interval)
62
+ this.interval = null
63
+ }
64
+ process.stdout.write(`\r\x1b[K`)
65
+ process.stdout.write("\x1b[?25h")
66
+ }
67
+ }
68
+
69
+ let loadSpinner: Spinner | null = null
70
+ let receiveSpinner: Spinner | null = null
71
+ let openSpinner: Spinner | null = null
72
+
73
+ export const log = {
74
+ header: (goal: string) => {
75
+ if (silent) return
76
+ console.log()
77
+ console.log(`${c.yellow}${icons.play}${c.reset} ${goal}`)
78
+ console.log()
79
+ },
80
+
81
+ config: (opts: { mo: string; model: string; headless: boolean; video: boolean; vision: boolean; steps: number }) => {
82
+ if (silent) return
83
+ const parts = [
84
+ `${c.dim}${opts.model}${c.reset}`,
85
+ opts.headless ? `${c.dim}headless${c.reset}` : null,
86
+ opts.video ? `${c.dim}video${c.reset}` : null,
87
+ opts.vision ? `${c.dim}vision${c.reset}` : null,
88
+ `${c.dim}steps:${c.reset} ${opts.steps}`,
89
+ ].filter(Boolean)
90
+ console.log(` ${parts.join(" ")}`)
91
+ console.log()
92
+ },
93
+
94
+ plan: (text: string) => {
95
+ if (silent) return
96
+ console.log(renderMd(text))
97
+ console.log()
98
+ },
99
+
100
+ step: (n: number, total: number, url: string) => {
101
+ if (silent) return
102
+ const shortUrl = url.length > 50 ? url.slice(0, 47) + "..." : url
103
+ console.log(`${c.magenta}${icons.dot}${c.reset} ${c.bold}${n}/${total}${c.reset} ${c.dim}${shortUrl}${c.reset}`)
104
+ },
105
+
106
+ thought: (msg: string) => {
107
+ if (silent) return
108
+ const short = msg.length > 80 ? msg.slice(0, 77) + "..." : msg
109
+ console.log(` ${c.bold}${c.yellow}${icons.brain}${c.reset} ${c.gray}${short}${c.reset}`)
110
+ },
111
+
112
+ action: (type: string, target?: string) => {
113
+ if (silent) return
114
+ const t = target ? ` ${c.dim}${target}${c.reset}` : ""
115
+ console.log(` ${c.blue}${icons.arrow}${c.reset} ${type}${t}`)
116
+ },
117
+
118
+ ok: (msg?: string) => {
119
+ if (silent) return
120
+ if (msg) {
121
+ const short = msg.length > 60 ? msg.slice(0, 57) + "..." : msg
122
+ console.log(` ${c.green}${icons.check}${c.reset} ${c.dim}${short}${c.reset}`)
123
+ }
124
+ },
125
+
126
+ fail: (msg: string) => {
127
+ if (silent) return
128
+ const short = msg.length > 60 ? msg.slice(0, 57) + "..." : msg
129
+ console.log(` ${c.red}${icons.cross}${c.reset} ${short}`)
130
+ },
131
+
132
+ done: (steps: number, reason?: string) => {
133
+ if (silent) return
134
+ console.log()
135
+ console.log(`${c.green}${icons.check} done${c.reset} ${c.dim}in ${steps} steps${c.reset}`)
136
+ if (reason) {
137
+ console.log()
138
+ console.log(renderMd(reason))
139
+ }
140
+ },
141
+
142
+ stats: (totalMs: number, aiMs: number, browserMs: number) => {
143
+ if (silent) return
144
+ const fmt = (ms: number) => (ms / 1000).toFixed(1) + "s"
145
+ console.log()
146
+ console.log(`${c.dim}total:${c.reset} ${fmt(totalMs)}`)
147
+ console.log(`${c.dim}neuro:${c.reset} ${fmt(aiMs)} ${c.dim}(${Math.round(aiMs / totalMs * 100)}%)${c.reset}`)
148
+ console.log(`${c.dim}browser:${c.reset} ${fmt(browserMs)} ${c.dim}(${Math.round(browserMs / totalMs * 100)}%)${c.reset}`)
149
+ },
150
+
151
+ video: (path: string) => {
152
+ if (silent) return
153
+ console.log(`${c.dim}${icons.circle} video: ${path}${c.reset}`)
154
+ },
155
+
156
+ error: (msg: string) => {
157
+ // errors always print, even in silent mode
158
+ console.error(`${c.red}${icons.cross} ${msg}${c.reset}`)
159
+ },
160
+
161
+ info: (msg: string) => {
162
+ if (silent) return
163
+ console.log(`${c.cyan}${icons.arrow}${c.reset} ${msg}`)
164
+ },
165
+
166
+ success: (msg: string) => {
167
+ if (silent) return
168
+ console.log(`${c.green}${icons.check}${c.reset} ${msg}`)
169
+ },
170
+
171
+ planning: () => {
172
+ if (silent) return
173
+ process.stdout.write(`${c.dim}planning...${c.reset}`)
174
+ },
175
+
176
+ planDone: () => {
177
+ if (silent) return
178
+ process.stdout.write(`\r${c.dim}planning... done${c.reset}\n\n`)
179
+ },
180
+
181
+ loadStart: () => {
182
+ if (silent) return
183
+ loadSpinner = new Spinner("load")
184
+ loadSpinner.start()
185
+ },
186
+
187
+ loadStop: () => {
188
+ if (silent) return
189
+ if (loadSpinner) {
190
+ loadSpinner.stop()
191
+ loadSpinner = null
192
+ }
193
+ },
194
+
195
+ receiveStart: () => {
196
+ if (silent) return
197
+ receiveSpinner = new Spinner("receive")
198
+ receiveSpinner.start()
199
+ },
200
+
201
+ receiveStop: () => {
202
+ if (silent) return
203
+ if (receiveSpinner) {
204
+ receiveSpinner.stop()
205
+ receiveSpinner = null
206
+ }
207
+ },
208
+
209
+ openStart: () => {
210
+ if (silent) return
211
+ openSpinner = new Spinner("opening")
212
+ openSpinner.start()
213
+ },
214
+
215
+ openStop: () => {
216
+ if (silent) return
217
+ if (openSpinner) {
218
+ openSpinner.stop()
219
+ openSpinner = null
220
+ }
221
+ },
222
+ }
@@ -0,0 +1,153 @@
1
+ import { spawn, type Subprocess } from "bun"
2
+ import { homedir } from "os"
3
+ import { join } from "path"
4
+ import { log } from "./log"
5
+
6
+ const MO_REPO = "zarazaex69/mo"
7
+ const CONFIG_DIR = join(homedir(), ".config", "traw")
8
+ const MO_BIN = join(CONFIG_DIR, "mo")
9
+ const MO_CONFIG_DIR = join(CONFIG_DIR, "configs")
10
+ const MO_CONFIG = join(MO_CONFIG_DIR, "config.yaml")
11
+ const CONFIG_URL = "https://raw.githubusercontent.com/zarazaex69/mo/main/configs/config.yaml"
12
+
13
+ let moProcess: Subprocess | null = null
14
+
15
+ function getPlatformAsset(): string {
16
+ const platform = process.platform
17
+ const arch = process.arch
18
+
19
+ if (platform === "linux" && arch === "x64") return "mo-linux-amd64"
20
+ if (platform === "linux" && arch === "arm64") return "mo-linux-arm64"
21
+ if (platform === "darwin" && arch === "x64") return "mo-darwin-amd64"
22
+ if (platform === "darwin" && arch === "arm64") return "mo-darwin-arm64"
23
+
24
+ throw new Error(`unsupported platform: ${platform}-${arch}`)
25
+ }
26
+
27
+ async function getLatestRelease(): Promise<{ tag: string; url: string }> {
28
+ const resp = await fetch(`https://api.github.com/repos/${MO_REPO}/releases/latest`)
29
+ if (!resp.ok) throw new Error("failed to fetch latest release")
30
+
31
+ const data = await resp.json() as { tag_name: string; assets: { name: string; browser_download_url: string }[] }
32
+ const asset = getPlatformAsset()
33
+ const tarball = data.assets.find(a => a.name === `${asset}.tar.gz`)
34
+
35
+ if (!tarball) throw new Error(`no binary for ${asset}`)
36
+
37
+ return { tag: data.tag_name, url: tarball.browser_download_url }
38
+ }
39
+
40
+ export async function pingMo(url: string): Promise<boolean> {
41
+ try {
42
+ const resp = await fetch(`${url}/health`, { signal: AbortSignal.timeout(2000) })
43
+ return resp.ok
44
+ } catch {
45
+ return false
46
+ }
47
+ }
48
+
49
+ export async function isMoInstalled(): Promise<boolean> {
50
+ return await Bun.file(MO_BIN).exists()
51
+ }
52
+
53
+ export async function downloadMo(): Promise<void> {
54
+ log.info("fetching latest mo release...")
55
+
56
+ const { tag, url } = await getLatestRelease()
57
+ log.info(`downloading mo ${tag}...`)
58
+
59
+ // ensure config dir exists
60
+ await Bun.write(join(CONFIG_DIR, ".keep"), "")
61
+ await Bun.write(join(MO_CONFIG_DIR, ".keep"), "")
62
+
63
+ const resp = await fetch(url)
64
+ if (!resp.ok) throw new Error("download failed")
65
+
66
+ const tarPath = join(CONFIG_DIR, "mo.tar.gz")
67
+ await Bun.write(tarPath, resp)
68
+
69
+ // extract tarball
70
+ const proc = spawn(["tar", "-xzf", tarPath, "-C", CONFIG_DIR], { stdout: "ignore", stderr: "pipe" })
71
+ await proc.exited
72
+
73
+ if (proc.exitCode !== 0) {
74
+ throw new Error("failed to extract mo")
75
+ }
76
+
77
+ // rename extracted binary to just "mo"
78
+ const asset = getPlatformAsset()
79
+ const extractedPath = join(CONFIG_DIR, asset)
80
+
81
+ if (await Bun.file(extractedPath).exists()) {
82
+ const content = await Bun.file(extractedPath).arrayBuffer()
83
+ await Bun.write(MO_BIN, content)
84
+ await Bun.spawn(["rm", extractedPath]).exited
85
+ }
86
+
87
+ // make executable
88
+ await Bun.spawn(["chmod", "+x", MO_BIN]).exited
89
+
90
+ // cleanup
91
+ await Bun.spawn(["rm", tarPath]).exited
92
+
93
+ // download config if not exists
94
+ if (!await Bun.file(MO_CONFIG).exists()) {
95
+ log.info("downloading config...")
96
+ const cfgResp = await fetch(CONFIG_URL)
97
+ if (cfgResp.ok) {
98
+ await Bun.write(MO_CONFIG, cfgResp)
99
+ }
100
+ }
101
+
102
+ log.success(`mo ${tag} installed`)
103
+ }
104
+
105
+ export async function startMo(port: number): Promise<void> {
106
+ if (!await isMoInstalled()) {
107
+ throw new Error("mo not installed")
108
+ }
109
+
110
+ log.info("starting mo server...")
111
+
112
+ const configExists = await Bun.file(MO_CONFIG).exists()
113
+ if (!configExists) {
114
+ throw new Error(`config not found: ${MO_CONFIG}`)
115
+ }
116
+
117
+ moProcess = spawn([MO_BIN, "--config", MO_CONFIG, "--port", String(port)], {
118
+ stdout: "ignore",
119
+ stderr: "ignore",
120
+ env: { ...process.env, MO_DATA_PATH: CONFIG_DIR },
121
+ })
122
+
123
+ // wait for server to be ready
124
+ const maxWait = 10000
125
+ const start = Date.now()
126
+
127
+ while (Date.now() - start < maxWait) {
128
+ // check if process died
129
+ if (moProcess.exitCode !== null) {
130
+ throw new Error(`mo exited with code ${moProcess.exitCode}`)
131
+ }
132
+
133
+ if (await pingMo(`http://localhost:${port}`)) {
134
+ log.success("mo server ready")
135
+ return
136
+ }
137
+ await Bun.sleep(300)
138
+ }
139
+
140
+ throw new Error("mo server failed to start (timeout)")
141
+ }
142
+
143
+ export function stopMo(): void {
144
+ if (moProcess) {
145
+ moProcess.kill()
146
+ moProcess = null
147
+ }
148
+ }
149
+
150
+ // cleanup on exit
151
+ process.on("exit", stopMo)
152
+ process.on("SIGINT", () => { stopMo(); process.exit(0) })
153
+ process.on("SIGTERM", () => { stopMo(); process.exit(0) })
@@ -0,0 +1,31 @@
1
+ // desktop notifications via notify-send (linux)
2
+ import { $ } from "bun"
3
+
4
+ let hasNotifySend: boolean | null = null
5
+
6
+ export async function checkNotify(): Promise<boolean> {
7
+ if (hasNotifySend !== null) return hasNotifySend
8
+
9
+ try {
10
+ await $`which notify-send`.quiet()
11
+ hasNotifySend = true
12
+ } catch {
13
+ hasNotifySend = false
14
+ }
15
+
16
+ return hasNotifySend
17
+ }
18
+
19
+ export async function notify(title: string, body?: string): Promise<void> {
20
+ if (!hasNotifySend) return
21
+
22
+ try {
23
+ if (body) {
24
+ await $`notify-send ${title} ${body}`.quiet()
25
+ } else {
26
+ await $`notify-send ${title}`.quiet()
27
+ }
28
+ } catch {
29
+ // ignore notification errors
30
+ }
31
+ }
@@ -0,0 +1,39 @@
1
+ import pkg from "../../package.json"
2
+
3
+ export const VERSION = pkg.version
4
+
5
+ const GITHUB_REPO = "zarazaex69/traw"
6
+ const RELEASES_API = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`
7
+
8
+ interface GithubRelease {
9
+ tag_name: string
10
+ html_url: string
11
+ }
12
+
13
+ export async function checkForUpdates(): Promise<{
14
+ hasUpdate: boolean
15
+ current: string
16
+ latest: string
17
+ url: string
18
+ } | null> {
19
+ try {
20
+ const resp = await fetch(RELEASES_API, {
21
+ headers: { "User-Agent": "traw-cli" },
22
+ })
23
+
24
+ if (!resp.ok) return null
25
+
26
+ const release = await resp.json() as GithubRelease
27
+ const latest = release.tag_name.replace(/^v/, "")
28
+ const current = VERSION.replace(/^v/, "")
29
+
30
+ return {
31
+ hasUpdate: latest !== current,
32
+ current: VERSION,
33
+ latest: release.tag_name,
34
+ url: release.html_url,
35
+ }
36
+ } catch {
37
+ return null
38
+ }
39
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext", "DOM"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": false,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }