@oh-my-pi/pi-mom 0.1.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/CHANGELOG.md +338 -0
- package/README.md +517 -0
- package/dev.sh +30 -0
- package/docker.sh +95 -0
- package/docs/artifacts-server.md +475 -0
- package/docs/events.md +307 -0
- package/docs/new.md +977 -0
- package/docs/sandbox.md +153 -0
- package/docs/slack-bot-minimal-guide.md +399 -0
- package/docs/v86.md +319 -0
- package/package.json +45 -0
- package/scripts/migrate-timestamps.ts +121 -0
- package/src/agent.ts +887 -0
- package/src/context.ts +666 -0
- package/src/download.ts +117 -0
- package/src/events.ts +385 -0
- package/src/log.ts +271 -0
- package/src/main.ts +334 -0
- package/src/sandbox.ts +238 -0
- package/src/slack.ts +635 -0
- package/src/store.ts +253 -0
- package/src/tools/attach.ts +47 -0
- package/src/tools/bash.ts +99 -0
- package/src/tools/edit.ts +165 -0
- package/src/tools/index.ts +19 -0
- package/src/tools/read.ts +165 -0
- package/src/tools/truncate.ts +236 -0
- package/src/tools/write.ts +45 -0
- package/tsconfig.build.json +9 -0
package/docs/v86.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# v86 Sandbox Evaluation
|
|
2
|
+
|
|
3
|
+
v86 is an x86 emulator written in JavaScript/WebAssembly that can run Linux in the browser or Node.js. This document details our evaluation for using it as a sandboxed execution environment.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- **What it is**: x86 PC emulator (32-bit, Pentium 4 level)
|
|
8
|
+
- **How it works**: Translates machine code to WebAssembly at runtime
|
|
9
|
+
- **Guest OS**: Alpine Linux 3.21 (32-bit x86)
|
|
10
|
+
- **Available packages**: Node.js 22, Python 3.12, git, curl, etc. (full Alpine repos)
|
|
11
|
+
|
|
12
|
+
## Key Findings
|
|
13
|
+
|
|
14
|
+
### What Works
|
|
15
|
+
|
|
16
|
+
| Feature | Status | Notes |
|
|
17
|
+
|---------|--------|-------|
|
|
18
|
+
| Outbound TCP | ✅ | HTTP, HTTPS, TLS all work |
|
|
19
|
+
| Outbound UDP | ✅ | DNS queries work |
|
|
20
|
+
| WebSocket client | ✅ | Can connect to external WebSocket servers |
|
|
21
|
+
| File I/O | ✅ | 9p filesystem for host<->guest file exchange |
|
|
22
|
+
| State save/restore | ✅ | ~80-100MB state files, instant resume |
|
|
23
|
+
| Package persistence | ✅ | Installed packages persist in saved state |
|
|
24
|
+
| npm install | ✅ | Works (outbound HTTPS) |
|
|
25
|
+
| git clone | ✅ | Works (outbound HTTPS) |
|
|
26
|
+
|
|
27
|
+
### What Doesn't Work
|
|
28
|
+
|
|
29
|
+
| Feature | Status | Notes |
|
|
30
|
+
|---------|--------|-------|
|
|
31
|
+
| Inbound connections | ❌ | VM is behind NAT (10.0.2.x), needs port forwarding |
|
|
32
|
+
| ICMP ping | ❌ | Userspace network stack limitation |
|
|
33
|
+
| 64-bit | ❌ | v86 only emulates 32-bit x86 |
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌─────────────────────────────────────────────────────────┐
|
|
39
|
+
│ Host (Node.js) │
|
|
40
|
+
│ │
|
|
41
|
+
│ ┌──────────────┐ ┌─────────────────────────────┐ │
|
|
42
|
+
│ │ rootlessRelay│◄───►│ v86 │ │
|
|
43
|
+
│ │ (WebSocket) │ │ ┌─────────────────────┐ │ │
|
|
44
|
+
│ │ │ │ │ Alpine Linux │ │ │
|
|
45
|
+
│ │ - DHCP │ │ │ - Node.js 22 │ │ │
|
|
46
|
+
│ │ - DNS proxy │ │ │ - Python 3.12 │ │ │
|
|
47
|
+
│ │ - NAT │ │ │ - etc. │ │ │
|
|
48
|
+
│ └──────────────┘ │ └─────────────────────┘ │ │
|
|
49
|
+
│ │ │ │ │ │
|
|
50
|
+
│ │ │ 9p filesystem │ │
|
|
51
|
+
│ ▼ │ │ │ │
|
|
52
|
+
│ Internet │ ▼ │ │
|
|
53
|
+
│ │ Host filesystem │ │
|
|
54
|
+
│ └─────────────────────────────┘ │
|
|
55
|
+
└─────────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Components & Sizes
|
|
59
|
+
|
|
60
|
+
| Component | Size | Purpose |
|
|
61
|
+
|-----------|------|---------|
|
|
62
|
+
| v86.wasm | ~2 MB | x86 emulator |
|
|
63
|
+
| libv86.mjs | ~330 KB | JavaScript runtime |
|
|
64
|
+
| seabios.bin | ~128 KB | BIOS |
|
|
65
|
+
| vgabios.bin | ~36 KB | VGA BIOS |
|
|
66
|
+
| Alpine rootfs | ~57 MB | Compressed filesystem (loaded on-demand) |
|
|
67
|
+
| alpine-fs.json | ~160 KB | Filesystem index |
|
|
68
|
+
| rootlessRelay | ~75 KB | Network relay |
|
|
69
|
+
| **Total** | **~60 MB** | Without saved state |
|
|
70
|
+
| Saved state | ~80-100 MB | Optional, for instant resume |
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install v86 ws
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Building the Alpine Image
|
|
79
|
+
|
|
80
|
+
v86 provides Docker tooling to build the Alpine image:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/copy/v86.git
|
|
84
|
+
cd v86/tools/docker/alpine
|
|
85
|
+
|
|
86
|
+
# Edit Dockerfile to add packages:
|
|
87
|
+
# ENV ADDPKGS=nodejs,npm,python3,git,curl
|
|
88
|
+
|
|
89
|
+
./build.sh
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This creates:
|
|
93
|
+
- `images/alpine-fs.json` - Filesystem index
|
|
94
|
+
- `images/alpine-rootfs-flat/` - Compressed file chunks
|
|
95
|
+
|
|
96
|
+
## Network Relay Setup
|
|
97
|
+
|
|
98
|
+
v86 needs a network relay for TCP/UDP connectivity. We use rootlessRelay:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/obegron/rootlessRelay.git
|
|
102
|
+
cd rootlessRelay
|
|
103
|
+
npm install
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Required Patches for Host Access
|
|
107
|
+
|
|
108
|
+
To allow the VM to connect to host services via the gateway IP (10.0.2.2), apply these patches to `relay.js`:
|
|
109
|
+
|
|
110
|
+
**Patch 1: Disable reverse TCP handling for gateway (line ~684)**
|
|
111
|
+
```javascript
|
|
112
|
+
// Change:
|
|
113
|
+
if (protocol === 6 && dstIP === GATEWAY_IP) {
|
|
114
|
+
this.handleReverseTCP(ipPacket);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// To:
|
|
119
|
+
if (false && protocol === 6 && dstIP === GATEWAY_IP) { // PATCHED
|
|
120
|
+
this.handleReverseTCP(ipPacket);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Patch 2: Redirect gateway TCP to localhost (line ~792)**
|
|
126
|
+
```javascript
|
|
127
|
+
// Change:
|
|
128
|
+
const socket = net.connect(dstPort, dstIP, () => {
|
|
129
|
+
|
|
130
|
+
// To:
|
|
131
|
+
const actualDstIP = dstIP === GATEWAY_IP ? "127.0.0.1" : dstIP;
|
|
132
|
+
const socket = net.connect(dstPort, actualDstIP, () => {
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Patch 3: Redirect gateway UDP to localhost (lines ~1431 and ~1449)**
|
|
136
|
+
```javascript
|
|
137
|
+
// Change:
|
|
138
|
+
this.udpSocket.send(payload, dstPort, dstIP, (err) => {
|
|
139
|
+
|
|
140
|
+
// To:
|
|
141
|
+
const actualUdpDstIP = dstIP === GATEWAY_IP ? "127.0.0.1" : dstIP;
|
|
142
|
+
this.udpSocket.send(payload, dstPort, actualUdpDstIP, (err) => {
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Starting the Relay
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
ENABLE_WSS=false LOG_LEVEL=1 node relay.js
|
|
149
|
+
# Listens on ws://127.0.0.1:8086/
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Basic Usage
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
import { V86 } from "v86";
|
|
156
|
+
import path from "node:path";
|
|
157
|
+
|
|
158
|
+
const emulator = new V86({
|
|
159
|
+
wasm_path: path.join(__dirname, "node_modules/v86/build/v86.wasm"),
|
|
160
|
+
bios: { url: path.join(__dirname, "bios/seabios.bin") },
|
|
161
|
+
vga_bios: { url: path.join(__dirname, "bios/vgabios.bin") },
|
|
162
|
+
filesystem: {
|
|
163
|
+
basefs: path.join(__dirname, "images/alpine-fs.json"),
|
|
164
|
+
baseurl: path.join(__dirname, "images/alpine-rootfs-flat/"),
|
|
165
|
+
},
|
|
166
|
+
autostart: true,
|
|
167
|
+
memory_size: 512 * 1024 * 1024,
|
|
168
|
+
bzimage_initrd_from_filesystem: true,
|
|
169
|
+
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable console=ttyS0",
|
|
170
|
+
net_device: {
|
|
171
|
+
type: "virtio",
|
|
172
|
+
relay_url: "ws://127.0.0.1:8086/",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Capture output
|
|
177
|
+
emulator.add_listener("serial0-output-byte", (byte) => {
|
|
178
|
+
process.stdout.write(String.fromCharCode(byte));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Send commands
|
|
182
|
+
emulator.serial0_send("echo hello\n");
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Communication Methods
|
|
186
|
+
|
|
187
|
+
### 1. Serial Console (stdin/stdout)
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
// Send command
|
|
191
|
+
emulator.serial0_send("ls -la\n");
|
|
192
|
+
|
|
193
|
+
// Receive output
|
|
194
|
+
let output = "";
|
|
195
|
+
emulator.add_listener("serial0-output-byte", (byte) => {
|
|
196
|
+
output += String.fromCharCode(byte);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 2. 9p Filesystem (file I/O)
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
// Write file to VM
|
|
204
|
+
const data = new TextEncoder().encode("#!/bin/sh\necho hello\n");
|
|
205
|
+
await emulator.create_file("/tmp/script.sh", data);
|
|
206
|
+
|
|
207
|
+
// Read file from VM
|
|
208
|
+
const result = await emulator.read_file("/tmp/output.txt");
|
|
209
|
+
console.log(new TextDecoder().decode(result));
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 3. Network (TCP to host services)
|
|
213
|
+
|
|
214
|
+
From inside the VM, connect to `10.0.2.2:PORT` to reach `localhost:PORT` on the host (requires patched relay).
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Inside VM
|
|
218
|
+
wget http://10.0.2.2:8080/ # Connects to host's localhost:8080
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## State Save/Restore
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
// Save state (includes all installed packages, files, etc.)
|
|
225
|
+
const state = await emulator.save_state();
|
|
226
|
+
fs.writeFileSync("vm-state.bin", Buffer.from(state));
|
|
227
|
+
|
|
228
|
+
// Restore state (instant resume, ~2 seconds)
|
|
229
|
+
const stateBuffer = fs.readFileSync("vm-state.bin");
|
|
230
|
+
await emulator.restore_state(stateBuffer.buffer);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Network Setup Inside VM
|
|
234
|
+
|
|
235
|
+
After boot, run these commands to enable networking:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
modprobe virtio-net
|
|
239
|
+
ip link set eth0 up
|
|
240
|
+
udhcpc -i eth0
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Or as a one-liner:
|
|
244
|
+
```bash
|
|
245
|
+
modprobe virtio-net && ip link set eth0 up && udhcpc -i eth0
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The VM will get IP `10.0.2.15` (or similar) via DHCP from the relay.
|
|
249
|
+
|
|
250
|
+
## Performance
|
|
251
|
+
|
|
252
|
+
| Metric | Value |
|
|
253
|
+
|--------|-------|
|
|
254
|
+
| Cold boot | ~20-25 seconds |
|
|
255
|
+
| State restore | ~2-3 seconds |
|
|
256
|
+
| Memory usage | ~512 MB (configurable) |
|
|
257
|
+
|
|
258
|
+
## Typical Workflow for Mom
|
|
259
|
+
|
|
260
|
+
1. **First run**:
|
|
261
|
+
- Start rootlessRelay
|
|
262
|
+
- Boot v86 with Alpine (~25s)
|
|
263
|
+
- Setup network
|
|
264
|
+
- Install needed packages (`apk add nodejs npm python3 git`)
|
|
265
|
+
- Save state
|
|
266
|
+
|
|
267
|
+
2. **Subsequent runs**:
|
|
268
|
+
- Start rootlessRelay
|
|
269
|
+
- Restore saved state (~2s)
|
|
270
|
+
- Ready to execute commands
|
|
271
|
+
|
|
272
|
+
3. **Command execution**:
|
|
273
|
+
- Send commands via `serial0_send()`
|
|
274
|
+
- Capture output via `serial0-output-byte` listener
|
|
275
|
+
- Exchange files via 9p filesystem
|
|
276
|
+
|
|
277
|
+
## Alternative: fetch Backend (No Relay Needed)
|
|
278
|
+
|
|
279
|
+
For HTTP-only networking, v86 has a built-in `fetch` backend:
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
net_device: {
|
|
283
|
+
type: "virtio",
|
|
284
|
+
relay_url: "fetch",
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
This uses the browser/Node.js `fetch()` API for HTTP requests. Limitations:
|
|
289
|
+
- Only HTTP/HTTPS (no raw TCP/UDP)
|
|
290
|
+
- No WebSocket
|
|
291
|
+
- Host access via `http://<port>.external` (e.g., `http://8080.external`)
|
|
292
|
+
|
|
293
|
+
## Files Reference
|
|
294
|
+
|
|
295
|
+
After building, you need these files:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
project/
|
|
299
|
+
├── node_modules/v86/build/
|
|
300
|
+
│ ├── v86.wasm
|
|
301
|
+
│ └── libv86.mjs
|
|
302
|
+
├── bios/
|
|
303
|
+
│ ├── seabios.bin
|
|
304
|
+
│ └── vgabios.bin
|
|
305
|
+
├── images/
|
|
306
|
+
│ ├── alpine-fs.json
|
|
307
|
+
│ └── alpine-rootfs-flat/
|
|
308
|
+
│ └── *.bin.zst (many files)
|
|
309
|
+
└── rootlessRelay/
|
|
310
|
+
└── relay.js (patched)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Resources
|
|
314
|
+
|
|
315
|
+
- [v86 GitHub](https://github.com/copy/v86)
|
|
316
|
+
- [v86 Networking Docs](https://github.com/copy/v86/blob/master/docs/networking.md)
|
|
317
|
+
- [v86 Alpine Setup](https://github.com/copy/v86/tree/master/tools/docker/alpine)
|
|
318
|
+
- [rootlessRelay](https://github.com/obegron/rootlessRelay)
|
|
319
|
+
- [v86 npm package](https://www.npmjs.com/package/v86)
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oh-my-pi/pi-mom",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Slack bot that delegates messages to the omp coding agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mom": "src/main.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"prepublishOnly": "echo 'Publishing bun-native package'"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.16",
|
|
14
|
+
"@oh-my-pi/pi-agent-core": "0.1.0",
|
|
15
|
+
"@oh-my-pi/pi-ai": "0.1.0",
|
|
16
|
+
"@oh-my-pi/pi-coding-agent": "0.1.0",
|
|
17
|
+
"@sinclair/typebox": "^0.34.0",
|
|
18
|
+
"@slack/socket-mode": "^2.0.0",
|
|
19
|
+
"@slack/web-api": "^7.0.0",
|
|
20
|
+
"async-mutex": "^0.5.0",
|
|
21
|
+
"chalk": "^5.6.2",
|
|
22
|
+
"croner": "^9.1.0",
|
|
23
|
+
"diff": "^8.0.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/diff": "^7.0.2",
|
|
27
|
+
"@types/node": "^24.3.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"slack",
|
|
31
|
+
"bot",
|
|
32
|
+
"ai",
|
|
33
|
+
"agent"
|
|
34
|
+
],
|
|
35
|
+
"author": "Mario Zechner",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/can1357/oh-my-pi.git",
|
|
40
|
+
"directory": "packages/mom"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"bun": ">=1.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Migrate log.jsonl timestamps from milliseconds to Slack format (seconds.microseconds)
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx tsx scripts/migrate-timestamps.ts <data-dir>
|
|
6
|
+
* Example: npx tsx scripts/migrate-timestamps.ts ./data
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
function isMillisecondTimestamp(ts: string): boolean {
|
|
13
|
+
// Slack timestamps are seconds.microseconds, like "1764279530.533489"
|
|
14
|
+
// Millisecond timestamps are just big numbers, like "1764279320398"
|
|
15
|
+
//
|
|
16
|
+
// Key insight:
|
|
17
|
+
// - Slack ts from 2025: ~1.7 billion (10 digits before decimal)
|
|
18
|
+
// - Millisecond ts from 2025: ~1.7 trillion (13 digits)
|
|
19
|
+
|
|
20
|
+
// If it has a decimal and the integer part is < 10^12, it's Slack format
|
|
21
|
+
if (ts.includes(".")) {
|
|
22
|
+
const intPart = parseInt(ts.split(".")[0], 10);
|
|
23
|
+
return intPart > 1e12; // Unlikely to have decimal AND be millis, but check anyway
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// No decimal - check if it's too big to be seconds
|
|
27
|
+
const num = parseInt(ts, 10);
|
|
28
|
+
return num > 1e12; // If > 1 trillion, it's milliseconds
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convertToSlackTs(msTs: string): string {
|
|
32
|
+
const ms = parseInt(msTs, 10);
|
|
33
|
+
const seconds = Math.floor(ms / 1000);
|
|
34
|
+
const micros = (ms % 1000) * 1000;
|
|
35
|
+
return `${seconds}.${micros.toString().padStart(6, "0")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function migrateFile(filePath: string): { total: number; migrated: number } {
|
|
39
|
+
const content = readFileSync(filePath, "utf-8");
|
|
40
|
+
const lines = content.split("\n").filter(Boolean);
|
|
41
|
+
|
|
42
|
+
let migrated = 0;
|
|
43
|
+
const newLines: string[] = [];
|
|
44
|
+
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
try {
|
|
47
|
+
const msg = JSON.parse(line);
|
|
48
|
+
if (msg.ts && isMillisecondTimestamp(msg.ts)) {
|
|
49
|
+
const oldTs = msg.ts;
|
|
50
|
+
msg.ts = convertToSlackTs(msg.ts);
|
|
51
|
+
console.log(` Converted: ${oldTs} -> ${msg.ts}`);
|
|
52
|
+
migrated++;
|
|
53
|
+
}
|
|
54
|
+
newLines.push(JSON.stringify(msg));
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Keep malformed lines as-is
|
|
57
|
+
console.log(` Warning: Could not parse line: ${line.substring(0, 50)}...`);
|
|
58
|
+
newLines.push(line);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (migrated > 0) {
|
|
63
|
+
writeFileSync(filePath, newLines.join("\n") + "\n", "utf-8");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { total: lines.length, migrated };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findLogFiles(dir: string): string[] {
|
|
70
|
+
const logFiles: string[] = [];
|
|
71
|
+
|
|
72
|
+
if (!existsSync(dir)) {
|
|
73
|
+
console.error(`Directory not found: ${dir}`);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const entries = readdirSync(dir);
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = join(dir, entry);
|
|
80
|
+
const stat = statSync(fullPath);
|
|
81
|
+
|
|
82
|
+
if (stat.isDirectory()) {
|
|
83
|
+
// Check for log.jsonl in subdirectory
|
|
84
|
+
const logPath = join(fullPath, "log.jsonl");
|
|
85
|
+
if (existsSync(logPath)) {
|
|
86
|
+
logFiles.push(logPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return logFiles;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Main
|
|
95
|
+
const dataDir = process.argv[2];
|
|
96
|
+
if (!dataDir) {
|
|
97
|
+
console.error("Usage: npx tsx scripts/migrate-timestamps.ts <data-dir>");
|
|
98
|
+
console.error("Example: npx tsx scripts/migrate-timestamps.ts ./data");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`Scanning for log.jsonl files in: ${dataDir}\n`);
|
|
103
|
+
|
|
104
|
+
const logFiles = findLogFiles(dataDir);
|
|
105
|
+
if (logFiles.length === 0) {
|
|
106
|
+
console.log("No log.jsonl files found.");
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let totalMigrated = 0;
|
|
111
|
+
let totalMessages = 0;
|
|
112
|
+
|
|
113
|
+
for (const logFile of logFiles) {
|
|
114
|
+
console.log(`Processing: ${logFile}`);
|
|
115
|
+
const { total, migrated } = migrateFile(logFile);
|
|
116
|
+
totalMessages += total;
|
|
117
|
+
totalMigrated += migrated;
|
|
118
|
+
console.log(` ${migrated}/${total} messages migrated\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`Done! Migrated ${totalMigrated}/${totalMessages} total messages across ${logFiles.length} files.`);
|