@nocoo/eagle-agent 0.3.0 → 0.5.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.
@@ -0,0 +1,379 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createConnection } from "node:net";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { promisify } from "node:util";
6
+ import WebSocket from "ws";
7
+ import { AgentMessageSchema, BridgeMessageSchema, TopologySchema, } from "../src/shared/realtime.js";
8
+ import { checkUrl, normalizeSnapshot, redact, } from "./collector.js";
9
+ import { submitTerminalInput } from "./terminal-input.js";
10
+ export function redactScreen(value, secrets) {
11
+ // A viewport can contain only a PEM body. Conservatively hide base64 line runs,
12
+ // including narrow terminal wraps, even when neither boundary is visible.
13
+ const text = redact(value, secrets).replace(/(?:^[ \t]*[A-Za-z0-9+/]{16,}={0,2}[ \t]*(?:\r?\n|$)){2,}/gm, "[REDACTED KEY]\n");
14
+ const positions = [];
15
+ let compact = "";
16
+ for (let i = 0; i < text.length; i++)
17
+ if (!/\s/.test(text[i])) {
18
+ positions.push(i);
19
+ compact += text[i];
20
+ }
21
+ const masked = new Set();
22
+ const mask = (start, length) => {
23
+ for (let i = start; i < start + length; i++)
24
+ masked.add(positions[i]);
25
+ };
26
+ // Match across rendered wraps; also suppress recognizable credential fragments at viewport edges.
27
+ for (const secret of secrets.filter(Boolean)) {
28
+ const size = Math.min(8, secret.length);
29
+ for (let i = 0; i <= secret.length - size; i++) {
30
+ const part = secret.slice(i, i + size);
31
+ for (let at = compact.indexOf(part); at >= 0; at = compact.indexOf(part, at + 1))
32
+ mask(at, size);
33
+ }
34
+ }
35
+ for (const match of compact.matchAll(/(?:eag1\.[A-Za-z0-9_.-]+|sk-[\w-]{16,}|gh[pousr]_[\w]{20,}|github_pat_[\w]{20,}|(?:TOKEN|SECRET|PASSWORD|API_KEY|CREDENTIAL)[\w]*[=:][^,;"']+)/gi))
36
+ mask(match.index, match[0].length);
37
+ return text
38
+ .split("")
39
+ .map((c, i) => (masked.has(i) ? "*" : c))
40
+ .join("")
41
+ .replace(/\*{8,}/g, "[REDACTED]");
42
+ }
43
+ export function socketRequest(path, method, params, signal) {
44
+ return new Promise((resolve, reject) => {
45
+ if (signal.aborted) {
46
+ reject(new Error("Cancelled"));
47
+ return;
48
+ }
49
+ const socket = createConnection(path);
50
+ const id = randomUUID();
51
+ let bytes = "", settled = false;
52
+ const finish = (error, result) => {
53
+ if (settled)
54
+ return;
55
+ settled = true;
56
+ clearTimeout(timer);
57
+ signal.removeEventListener("abort", abort);
58
+ socket.destroy();
59
+ if (error)
60
+ reject(error);
61
+ else if (result)
62
+ resolve(result);
63
+ else
64
+ reject(new Error("Missing response"));
65
+ };
66
+ const abort = () => finish(new Error("Cancelled"));
67
+ const timer = setTimeout(() => finish(new Error("Herdr timeout")), 5000);
68
+ signal.addEventListener("abort", abort, { once: true });
69
+ socket.setEncoding("utf8");
70
+ socket.on("error", () => finish(new Error("Herdr unavailable")));
71
+ socket.on("end", () => finish(new Error("Herdr disconnected")));
72
+ socket.on("connect", () => {
73
+ if (!signal.aborted)
74
+ socket.write(JSON.stringify({ id, method, params }) + "\n");
75
+ });
76
+ socket.on("data", (chunk) => {
77
+ bytes += chunk;
78
+ if (bytes.length > 4 * 1024 * 1024) {
79
+ finish(new Error("Herdr response too large"));
80
+ return;
81
+ }
82
+ const end = bytes.indexOf("\n");
83
+ if (end < 0)
84
+ return;
85
+ try {
86
+ const response = JSON.parse(bytes.slice(0, end));
87
+ if (response.id !== id || response.error || !response.result)
88
+ throw new Error();
89
+ finish(undefined, response.result);
90
+ }
91
+ catch {
92
+ finish(new Error("Invalid Herdr response"));
93
+ }
94
+ });
95
+ });
96
+ }
97
+ export class LiveBridge {
98
+ watches = new Map();
99
+ secrets;
100
+ send;
101
+ resolve;
102
+ constructor(secrets, send, resolve) {
103
+ this.secrets = secrets;
104
+ this.send = send;
105
+ this.resolve = resolve;
106
+ }
107
+ receive(value) {
108
+ const parsed = BridgeMessageSchema.safeParse(value);
109
+ if (!parsed.success)
110
+ throw new Error("Invalid relay message");
111
+ const message = parsed.data;
112
+ if (message.type === "subscriptions") {
113
+ const wanted = new Map(message.spaces.map((s) => [s.spaceId, s]));
114
+ for (const [key, watch] of this.watches)
115
+ if (wanted.get(key)?.subscriptionId !== watch.subscription.subscriptionId) {
116
+ watch.abort.abort();
117
+ this.watches.delete(key);
118
+ }
119
+ for (const subscription of message.spaces) {
120
+ const previous = this.watches.get(subscription.spaceId);
121
+ if (previous) {
122
+ previous.force = true;
123
+ continue;
124
+ }
125
+ const watch = {
126
+ subscription,
127
+ abort: new AbortController(),
128
+ force: true,
129
+ screens: new Map(),
130
+ frameSequence: 0,
131
+ queue: Promise.resolve(),
132
+ };
133
+ this.watches.set(subscription.spaceId, watch);
134
+ void this.watch(watch);
135
+ }
136
+ }
137
+ else if (message.type === "input") {
138
+ const watch = this.watches.get(message.spaceId);
139
+ const ack = (status) => this.send({
140
+ type: "ack",
141
+ clientId: message.clientId,
142
+ seq: message.seq,
143
+ status,
144
+ });
145
+ if (!watch ||
146
+ watch.subscription.subscriptionId !== message.subscriptionId) {
147
+ ack("rejected");
148
+ return;
149
+ }
150
+ watch.queue = watch.queue.then(async () => {
151
+ try {
152
+ if (!watch.path ||
153
+ watch.abort.signal.aborted ||
154
+ redact(message.text, this.secrets) !== message.text) {
155
+ ack("rejected");
156
+ return;
157
+ }
158
+ const result = await socketRequest(watch.path, "session.snapshot", {}, watch.abort.signal);
159
+ const raw = result.snapshot;
160
+ const pane = raw.panes.find((p) => p.pane_id === message.paneId &&
161
+ p.terminal_id === message.terminalId &&
162
+ `${message.spaceId.slice(0, message.spaceId.indexOf(":"))}:${p.workspace_id}` ===
163
+ message.spaceId);
164
+ if (!pane || watch.abort.signal.aborted) {
165
+ ack("rejected");
166
+ return;
167
+ }
168
+ const rect = raw.layouts
169
+ .find((l) => l.tab_id === pane.tab_id)
170
+ ?.panes.find((p) => p.pane_id === pane.pane_id)?.rect;
171
+ if (!rect) {
172
+ ack("rejected");
173
+ return;
174
+ }
175
+ const status = await submitTerminalInput(watch.path, message, { width: rect.width, height: Math.max(1, rect.height - 1) }, watch.abort.signal, async () => {
176
+ const latest = (await socketRequest(watch.path ?? "", "session.snapshot", {}, watch.abort.signal)).snapshot;
177
+ return latest.panes.some((p) => p.pane_id === pane.pane_id &&
178
+ p.terminal_id === message.terminalId &&
179
+ p.workspace_id === pane.workspace_id &&
180
+ p.tab_id === pane.tab_id);
181
+ });
182
+ watch.force = true;
183
+ ack(status);
184
+ }
185
+ catch {
186
+ ack("rejected");
187
+ }
188
+ });
189
+ }
190
+ }
191
+ async watch(watch) {
192
+ const { spaceId } = watch.subscription;
193
+ const session = spaceId.slice(0, spaceId.indexOf(":"));
194
+ const signal = watch.abort.signal;
195
+ try {
196
+ watch.path = await this.resolve(session, signal);
197
+ while (!signal.aborted) {
198
+ const result = await socketRequest(watch.path, "session.snapshot", {}, signal);
199
+ const raw = result.snapshot;
200
+ const space = normalizeSnapshot(raw, session, this.secrets).find((s) => s.id === spaceId);
201
+ if (!space)
202
+ throw new Error("Space unavailable");
203
+ const topology = TopologySchema.parse({
204
+ type: "topology",
205
+ ...watch.subscription,
206
+ tabs: space.tabs.map((t) => ({
207
+ id: t.id,
208
+ name: redactScreen(t.name, this.secrets).slice(0, 240),
209
+ panes: t.panes.map((p) => ({
210
+ id: p.id,
211
+ terminalId: raw.panes.find((r) => r.pane_id === p.id)
212
+ ?.terminal_id,
213
+ title: redactScreen(p.title, this.secrets).slice(0, 240),
214
+ rect: p.rect,
215
+ })),
216
+ })),
217
+ });
218
+ const force = watch.force ||
219
+ JSON.stringify(topology) !== JSON.stringify(watch.topology);
220
+ watch.force = false;
221
+ if (signal.aborted)
222
+ return;
223
+ if (force) {
224
+ this.send(topology);
225
+ watch.topology = topology;
226
+ }
227
+ const panes = topology.tabs.flatMap((t) => t.panes);
228
+ const pending = [];
229
+ // Herdr 0.9.1 screen revisions can be zero. Compare content after bounded reads.
230
+ for (let i = 0; i < panes.length; i += 4)
231
+ await Promise.all(panes.slice(i, i + 4).map(async (p) => {
232
+ const result = await socketRequest(watch.path ?? "", "pane.read", {
233
+ pane_id: p.id,
234
+ source: "visible",
235
+ format: "text",
236
+ strip_ansi: true,
237
+ }, signal);
238
+ const read = result.read;
239
+ const original = raw.panes.find((r) => r.pane_id === p.id);
240
+ if (read.pane_id !== p.id ||
241
+ read.workspace_id !== original?.workspace_id ||
242
+ read.tab_id !== original?.tab_id)
243
+ return;
244
+ pending.push({
245
+ pane: p,
246
+ text: redactScreen(read.text, this.secrets).slice(-32000),
247
+ tab: read.tab_id,
248
+ });
249
+ }));
250
+ const after = (await socketRequest(watch.path, "session.snapshot", {}, signal)).snapshot;
251
+ if (signal.aborted)
252
+ return;
253
+ for (const { pane: p, text, tab } of pending) {
254
+ const current = after.panes.find((r) => r.pane_id === p.id &&
255
+ r.terminal_id === p.terminalId &&
256
+ r.tab_id === tab &&
257
+ `${session}:${r.workspace_id}` === spaceId);
258
+ if (!current) {
259
+ watch.force = true;
260
+ continue;
261
+ }
262
+ if (!force && watch.screens.get(p.terminalId) === text)
263
+ continue;
264
+ this.send(AgentMessageSchema.parse({
265
+ type: "frame",
266
+ ...watch.subscription,
267
+ paneId: p.id,
268
+ terminalId: p.terminalId,
269
+ revision: ++watch.frameSequence,
270
+ text,
271
+ observedAt: new Date().toISOString(),
272
+ }));
273
+ watch.screens.set(p.terminalId, text);
274
+ }
275
+ for (const key of watch.screens.keys())
276
+ if (!panes.some((p) => p.terminalId === key))
277
+ watch.screens.delete(key);
278
+ await delay(350, undefined, { signal });
279
+ }
280
+ }
281
+ catch {
282
+ if (!signal.aborted)
283
+ this.send({ type: "unavailable", ...watch.subscription });
284
+ }
285
+ finally {
286
+ watch.abort.abort();
287
+ if (this.watches.get(spaceId) === watch)
288
+ this.watches.delete(spaceId);
289
+ }
290
+ }
291
+ close() {
292
+ for (const watch of this.watches.values())
293
+ watch.abort.abort();
294
+ this.watches.clear();
295
+ }
296
+ }
297
+ export async function realtimeWatch(config, signal) {
298
+ const url = new URL("/api/v1/realtime-agent", checkUrl(config.url));
299
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
300
+ let retry = 1000;
301
+ while (!signal.aborted) {
302
+ let authFailure = false;
303
+ await new Promise((resolve) => {
304
+ const ws = new WebSocket(url, {
305
+ headers: {
306
+ Authorization: `Bearer ${config.token}`,
307
+ "X-Eagle-Machine": config.machineId,
308
+ },
309
+ handshakeTimeout: 10000,
310
+ maxPayload: 262144,
311
+ followRedirects: false,
312
+ });
313
+ const bridge = new LiveBridge([config.token], (message) => {
314
+ if (ws.readyState !== WebSocket.OPEN)
315
+ return;
316
+ if (ws.bufferedAmount > 524288) {
317
+ bridge.close();
318
+ ws.terminate();
319
+ return;
320
+ }
321
+ ws.send(JSON.stringify(message));
322
+ }, async (session, watchSignal) => {
323
+ const { stdout } = await promisify(execFile)("herdr", ["session", "list", "--json"], { timeout: 8000, maxBuffer: 1048576, signal: watchSignal });
324
+ const found = JSON.parse(stdout).sessions.find((s) => s.name === session && s.running);
325
+ if (!found?.socket_path)
326
+ throw new Error("Session unavailable");
327
+ return found.socket_path;
328
+ });
329
+ let lastMessage = Date.now();
330
+ const heartbeat = setInterval(() => {
331
+ if (Date.now() - lastMessage > 30000) {
332
+ bridge.close();
333
+ ws.terminate();
334
+ }
335
+ else if (ws.readyState === WebSocket.OPEN)
336
+ ws.send(JSON.stringify({ type: "ping" }));
337
+ }, 10000);
338
+ const stop = () => {
339
+ bridge.close();
340
+ ws.terminate();
341
+ };
342
+ signal.addEventListener("abort", stop, { once: true });
343
+ ws.on("open", () => {
344
+ ws.send(JSON.stringify({ type: "ping" }));
345
+ retry = 1000;
346
+ console.log(JSON.stringify({ event: "realtime_connected" }));
347
+ });
348
+ ws.on("message", (data) => {
349
+ lastMessage = Date.now();
350
+ try {
351
+ bridge.receive(JSON.parse(String(data)));
352
+ }
353
+ catch {
354
+ bridge.close();
355
+ ws.terminate();
356
+ }
357
+ });
358
+ ws.on("unexpected-response", (_request, response) => {
359
+ bridge.close();
360
+ authFailure = [401, 403].includes(response.statusCode ?? 0);
361
+ response.destroy();
362
+ ws.terminate();
363
+ });
364
+ ws.on("error", () => { });
365
+ ws.on("close", () => {
366
+ clearInterval(heartbeat);
367
+ signal.removeEventListener("abort", stop);
368
+ bridge.close();
369
+ resolve();
370
+ });
371
+ });
372
+ if (authFailure)
373
+ throw new Error("Realtime authentication rejected; fix secure configuration");
374
+ if (!signal.aborted) {
375
+ await delay(retry, undefined, { signal }).catch(() => { });
376
+ retry = Math.min(retry * 2, 30000);
377
+ }
378
+ }
379
+ }
@@ -0,0 +1,210 @@
1
+ import { createConnection } from "node:net";
2
+ import { dirname, join, parse } from "node:path";
3
+ // Herdr 0.9.1 protocol 22: bincode standard, prefixed by a u32 LE length.
4
+ // Only strict AttachTerminal is used. ControlTerminal can fall back to a pane/name.
5
+ const integer = (n) => {
6
+ if (!Number.isSafeInteger(n) || n < 0 || n > 0xffffffff)
7
+ throw new Error("Invalid integer");
8
+ if (n < 251)
9
+ return Buffer.from([n]);
10
+ const b = Buffer.alloc(n <= 65535 ? 3 : 5);
11
+ b[0] = b.length === 3 ? 251 : 252;
12
+ if (b.length === 3)
13
+ b.writeUInt16LE(n, 1);
14
+ else
15
+ b.writeUInt32LE(n, 1);
16
+ return b;
17
+ };
18
+ const vector = (value) => {
19
+ const b = Buffer.from(value);
20
+ return Buffer.concat([integer(b.length), b]);
21
+ };
22
+ const frame = (...parts) => {
23
+ const b = Buffer.concat(parts);
24
+ const prefix = Buffer.alloc(4);
25
+ prefix.writeUInt32LE(b.length);
26
+ return Buffer.concat([prefix, b]);
27
+ };
28
+ class Reader {
29
+ at = 0;
30
+ data;
31
+ constructor(data) {
32
+ this.data = data;
33
+ }
34
+ number() {
35
+ const tag = this.data.readUInt8(this.at++);
36
+ if (tag < 251)
37
+ return tag;
38
+ const size = tag === 251 ? 2 : tag === 252 ? 4 : tag === 253 ? 8 : 0;
39
+ if (!size)
40
+ throw new Error("Invalid varint");
41
+ const n = size === 8
42
+ ? Number(this.data.readBigUInt64LE(this.at))
43
+ : this.data.readUIntLE(this.at, size);
44
+ this.at += size;
45
+ if (!Number.isSafeInteger(n))
46
+ throw new Error("Integer overflow");
47
+ return n;
48
+ }
49
+ bytes() {
50
+ const length = this.number();
51
+ if (length > this.data.length - this.at)
52
+ throw new Error("Invalid vector");
53
+ const b = this.data.subarray(this.at, this.at + length);
54
+ this.at += length;
55
+ return b;
56
+ }
57
+ done() {
58
+ if (this.at !== this.data.length)
59
+ throw new Error("Unexpected payload");
60
+ }
61
+ }
62
+ const basic = {
63
+ enter: [13, "\r"],
64
+ esc: [27, "\x1b"],
65
+ tab: [9, "\t"],
66
+ "shift+tab": [9, "\x1b[Z"],
67
+ backspace: [127, "\x7f"],
68
+ "ctrl+c": [99, "\x03"],
69
+ "ctrl+d": [100, "\x04"],
70
+ "ctrl+l": [108, "\x0c"],
71
+ };
72
+ function keyBytes(key, flags, modify) {
73
+ const [code, legacy] = basic[key];
74
+ const modifier = key.startsWith("ctrl+") ? 5 : key === "shift+tab" ? 2 : 1;
75
+ if (modify > 2)
76
+ throw new Error("Unsupported keyboard mode");
77
+ if (flags && (flags & 8 || modifier !== 1 || key === "esc")) {
78
+ const press = `\x1b[${code};${modifier}${flags & 2 ? ":1" : ""}u`;
79
+ const release = flags & 2 && (flags & 8 || key !== "shift+tab")
80
+ ? `\x1b[${code};${modifier}:3u`
81
+ : "";
82
+ return press + release;
83
+ }
84
+ return modify === 2 && modifier !== 1
85
+ ? `\x1b[27;${modifier};${code}~`
86
+ : legacy;
87
+ }
88
+ export async function submitTerminalInput(apiPath, input, size, signal, validate) {
89
+ if (signal.aborted ||
90
+ [...input.text].some((c) => (c.charCodeAt(0) < 32 && c !== "\n" && c !== "\t") ||
91
+ c.charCodeAt(0) === 127) ||
92
+ ![size.width, size.height].every((n) => Number.isInteger(n) && n > 0 && n <= 65535))
93
+ return "rejected";
94
+ return new Promise((resolve) => {
95
+ const socket = createConnection(join(dirname(apiPath), `${parse(apiPath).name}-client.sock`));
96
+ let bytes = Buffer.alloc(0), received = 0, welcomed = false, screen = false, sent = false, checking = false, settled = false;
97
+ let keyboard;
98
+ const finish = (status = sent ? "unknown" : "rejected") => {
99
+ if (settled)
100
+ return;
101
+ settled = true;
102
+ clearTimeout(timer);
103
+ signal.removeEventListener("abort", abort);
104
+ socket.destroy();
105
+ resolve(status);
106
+ };
107
+ const abort = () => finish();
108
+ const timer = setTimeout(abort, 5000);
109
+ signal.addEventListener("abort", abort, { once: true });
110
+ const send = async () => {
111
+ if (!screen || !keyboard || checking || settled)
112
+ return;
113
+ checking = true;
114
+ try {
115
+ if (!(await validate()) || signal.aborted || settled) {
116
+ finish();
117
+ return;
118
+ }
119
+ const mode = keyboard;
120
+ const keys = input.keys.map((key) => keyBytes(key, mode.flags, mode.modify));
121
+ const messages = [];
122
+ // Complete paste in its own Input: Herdr applies the runtime's paste mode.
123
+ if (input.text)
124
+ messages.push(frame(integer(1), vector(`\x1b[200~${input.text}\x1b[201~`)));
125
+ for (const key of keys)
126
+ messages.push(frame(integer(1), vector(key)));
127
+ messages.push(frame(integer(4)));
128
+ sent = true;
129
+ socket.write(Buffer.concat(messages));
130
+ }
131
+ catch {
132
+ finish();
133
+ }
134
+ };
135
+ socket.on("error", abort);
136
+ socket.on("end", abort);
137
+ socket.on("close", abort);
138
+ socket.on("connect", () => {
139
+ if (signal.aborted) {
140
+ finish();
141
+ return;
142
+ }
143
+ socket.write(frame(integer(0), integer(22), integer(size.width), integer(size.height), integer(0), integer(0), integer(0)));
144
+ });
145
+ socket.on("data", (chunk) => {
146
+ received += chunk.length;
147
+ if (received > 8 * 1024 * 1024) {
148
+ finish();
149
+ return;
150
+ }
151
+ bytes = Buffer.concat([bytes, Buffer.from(chunk)]);
152
+ try {
153
+ while (bytes.length >= 4 && !settled) {
154
+ const length = bytes.readUInt32LE();
155
+ if (length === 0 || length > 2 * 1024 * 1024)
156
+ throw new Error("Invalid frame size");
157
+ if (bytes.length < length + 4)
158
+ break;
159
+ const reader = new Reader(bytes.subarray(4, length + 4));
160
+ bytes = bytes.subarray(length + 4);
161
+ const type = reader.number();
162
+ if (!welcomed) {
163
+ if (type !== 0 ||
164
+ reader.number() !== 22 ||
165
+ reader.number() !== 1 ||
166
+ reader.number() !== 0)
167
+ throw new Error("Unsupported handshake");
168
+ reader.done();
169
+ welcomed = true;
170
+ socket.write(frame(integer(5), vector(input.terminalId), integer(0)));
171
+ }
172
+ else if (type === 1) {
173
+ reader.number();
174
+ const columns = reader.number(), rows = reader.number();
175
+ if (columns < 1 || rows < 1 || columns > 65535 || rows > 65535)
176
+ throw new Error("Invalid screen size");
177
+ const full = reader.number();
178
+ if (full > 1)
179
+ throw new Error("Invalid full-frame flag");
180
+ reader.bytes();
181
+ reader.done();
182
+ if (full === 1)
183
+ screen = true;
184
+ void send();
185
+ }
186
+ else if (type === 16) {
187
+ const flags = reader.number(), modify = reader.number();
188
+ reader.done();
189
+ if (flags > 31)
190
+ throw new Error("Unsupported keyboard mode");
191
+ keyboard = { flags, modify };
192
+ void send();
193
+ }
194
+ else if (type === 3) {
195
+ const option = reader.number();
196
+ if (option > 1)
197
+ throw new Error("Invalid option");
198
+ const reason = option === 1 ? reader.bytes().toString() : "";
199
+ reader.done();
200
+ finish(sent && reason === "detached" ? "submitted" : undefined);
201
+ }
202
+ // Graphics/notifications are discarded locally, never logged or forwarded.
203
+ }
204
+ }
205
+ catch {
206
+ finish();
207
+ }
208
+ });
209
+ });
210
+ }
@@ -1 +1,2 @@
1
- export const AGENT_VERSION = "0.3.0";
1
+ import pkg from "../package.json" with { type: "json" };
2
+ export const AGENT_VERSION = pkg.version;
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "eagle",
3
+ "version": "0.5.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --host 127.0.0.1 --port 7053 --strictPort",
8
+ "dev:api": "wrangler dev --port 37053 --inspector-port 38053",
9
+ "build": "vite build",
10
+ "typecheck": "tsc --noEmit",
11
+ "lint": "biome check .",
12
+ "format": "biome check --write .",
13
+ "test": "node --test tests/*.test.ts",
14
+ "test:browser": "playwright test",
15
+ "check": "npm run test && npm run typecheck && npm run lint && npm run build",
16
+ "deploy": "node scripts/deploy.ts",
17
+ "agent": "node agent/cli.ts",
18
+ "db:local": "wrangler d1 migrations apply eagle --local",
19
+ "db:remote": "wrangler d1 migrations apply eagle --remote"
20
+ },
21
+ "dependencies": {
22
+ "@ai-sdk/anthropic": "^4.0.58",
23
+ "@ai-sdk/openai": "^4.0.71",
24
+ "@nocoo/basalt": "2.1.8",
25
+ "@nocoo/next-ai": "^0.4.0",
26
+ "ai": "^7.0.107",
27
+ "jose": "^6.2.12",
28
+ "lucide-react": "^1.34.0",
29
+ "react": "^19.2.8",
30
+ "react-dom": "^19.2.8",
31
+ "recharts": "^3.10.1",
32
+ "ws": "^8.21.3",
33
+ "zod": "^4.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "@biomejs/biome": "2.5.10",
37
+ "@playwright/test": "^1.62.1",
38
+ "@tailwindcss/vite": "^4.3.3",
39
+ "@types/node": "^25.0.0",
40
+ "@types/react": "^19.2.18",
41
+ "@types/react-dom": "^19.2.5",
42
+ "@types/ws": "^8.18.1",
43
+ "@vitejs/plugin-react": "^6.1.0",
44
+ "miniflare": "5.20260918.0-alpha",
45
+ "tailwindcss": "^4.3.3",
46
+ "typescript": "7.0.2",
47
+ "vite": "^8.2.2",
48
+ "wrangler": "4.135.0"
49
+ }
50
+ }