vision-navigator 1.0.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/dist/server.js ADDED
@@ -0,0 +1,409 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startServer = startServer;
7
+ require("dotenv/config");
8
+ const express_1 = __importDefault(require("express"));
9
+ const navigator_1 = require("./navigator");
10
+ const storage_1 = require("./storage");
11
+ const path_1 = __importDefault(require("path"));
12
+ const js_yaml_1 = __importDefault(require("js-yaml"));
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const app = (0, express_1.default)();
15
+ app.use(express_1.default.json());
16
+ app.use(express_1.default.text({ type: ['text/yaml', 'application/x-yaml', 'application/yaml'] }));
17
+ const activeRuns = new Map();
18
+ const groqPlatformUsage = new Map();
19
+ const groqDailyLimit = Number(process.env.GROQ_DAILY_LIMIT || 5);
20
+ const storage = new storage_1.Storage();
21
+ // Serve static frontend and screenshots
22
+ app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
23
+ app.use('/screenshots', express_1.default.static(path_1.default.join(process.cwd(), 'screenshots')));
24
+ function currentDateKey() {
25
+ const d = new Date();
26
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
27
+ }
28
+ function getClientKey(req) {
29
+ const ip = (req.headers['x-forwarded-for'] || req.ip || '').toString();
30
+ return ip.split(',')[0].trim() || 'unknown';
31
+ }
32
+ function enforceGroqPlatformLimit(req, res) {
33
+ const hasPlatformKey = Boolean(process.env.GROQ_API_KEY) || String(process.env.OPENROUTER_API_KEY || '').startsWith('gsk_');
34
+ const byoKey = req.get('x-groq-api-key');
35
+ const requestedProvider = req.get('x-llm-provider');
36
+ if (requestedProvider && requestedProvider !== 'groq')
37
+ return true;
38
+ if (!hasPlatformKey || byoKey)
39
+ return true;
40
+ const key = getClientKey(req);
41
+ const date = currentDateKey();
42
+ const prev = groqPlatformUsage.get(key);
43
+ if (!prev || prev.date !== date) {
44
+ groqPlatformUsage.set(key, { date, count: 0 });
45
+ }
46
+ const now = groqPlatformUsage.get(key);
47
+ if (!now)
48
+ return true;
49
+ if (now.count >= groqDailyLimit) {
50
+ res.status(429).json({ error: `Demo limit reached (${groqDailyLimit} generations/day). Bring your own Groq API key.` });
51
+ return false;
52
+ }
53
+ now.count += 1;
54
+ groqPlatformUsage.set(key, now);
55
+ return true;
56
+ }
57
+ function makeNavigatorFromRequest(req) {
58
+ const groqApiKey = req.get('x-groq-api-key') || undefined;
59
+ const groqModel = req.get('x-groq-model') || undefined;
60
+ const provider = req.get('x-llm-provider') || undefined;
61
+ const normalizedProvider = provider === 'groq' || provider === 'openrouter' || provider === 'ollama' ? provider : undefined;
62
+ const hasEnvGroqKey = Boolean(process.env.GROQ_API_KEY) || String(process.env.OPENROUTER_API_KEY || '').startsWith('gsk_');
63
+ const effectiveProvider = normalizedProvider === 'groq' && !groqApiKey && !hasEnvGroqKey ? undefined : normalizedProvider;
64
+ return new navigator_1.Navigator(undefined, undefined, {
65
+ provider: effectiveProvider,
66
+ groqApiKey,
67
+ groqModel
68
+ });
69
+ }
70
+ app.get('/runs', (req, res) => {
71
+ try {
72
+ const historyPath = path_1.default.join(process.cwd(), 'screenshots', 'runs.json');
73
+ if (fs_1.default.existsSync(historyPath)) {
74
+ const history = JSON.parse(fs_1.default.readFileSync(historyPath, 'utf8'));
75
+ res.json(history.reverse()); // Newest first
76
+ }
77
+ else {
78
+ res.json([]);
79
+ }
80
+ }
81
+ catch (e) {
82
+ res.status(500).json({ error: String(e) });
83
+ }
84
+ });
85
+ app.post('/workflow/stream', async (req, res) => {
86
+ res.setHeader('Content-Type', 'text/event-stream');
87
+ res.setHeader('Cache-Control', 'no-cache');
88
+ res.setHeader('Connection', 'keep-alive');
89
+ try {
90
+ if (!enforceGroqPlatformLimit(req, res))
91
+ return;
92
+ let workflow = req.body;
93
+ const contentType = req.get('Content-Type');
94
+ if (contentType && (contentType.includes('yaml') || contentType.includes('yml'))) {
95
+ if (typeof req.body === 'string') {
96
+ try {
97
+ workflow = js_yaml_1.default.load(req.body);
98
+ }
99
+ catch (e) {
100
+ res.write(`data: ${JSON.stringify({ error: "Invalid YAML format: " + String(e) })}\n\n`);
101
+ return res.end();
102
+ }
103
+ }
104
+ }
105
+ if (!workflow || !workflow.steps) {
106
+ res.write(`data: ${JSON.stringify({ error: "Invalid workflow format" })}\n\n`);
107
+ return res.end();
108
+ }
109
+ const navigator = makeNavigatorFromRequest(req);
110
+ await navigator.ensureModelReady();
111
+ console.log("Starting streaming workflow execution...");
112
+ const runId = Date.now().toString();
113
+ const control = { aborted: false };
114
+ activeRuns.set(runId, { control });
115
+ res.write(`data: ${JSON.stringify({ started: true, runId })}\n\n`);
116
+ try {
117
+ const run = await navigator.runWorkflow(workflow, (stepResult) => {
118
+ res.write(`data: ${JSON.stringify({ step: stepResult, runId })}\n\n`);
119
+ }, { runId }, control);
120
+ res.write(`data: ${JSON.stringify({ done: true, run })}\n\n`);
121
+ res.end();
122
+ }
123
+ finally {
124
+ activeRuns.delete(runId);
125
+ }
126
+ }
127
+ catch (e) {
128
+ console.error("Server Error:", e);
129
+ res.write(`data: ${JSON.stringify({ error: String(e) })}\n\n`);
130
+ res.end();
131
+ }
132
+ });
133
+ app.post('/workflow', async (req, res) => {
134
+ try {
135
+ if (!enforceGroqPlatformLimit(req, res))
136
+ return;
137
+ let workflow = req.body;
138
+ // Check if body is string (YAML) and parse it
139
+ const contentType = req.get('Content-Type');
140
+ if (contentType && (contentType.includes('yaml') || contentType.includes('yml'))) {
141
+ if (typeof req.body === 'string') {
142
+ try {
143
+ workflow = js_yaml_1.default.load(req.body);
144
+ }
145
+ catch (e) {
146
+ return res.status(400).json({ error: "Invalid YAML format: " + String(e) });
147
+ }
148
+ }
149
+ }
150
+ if (!workflow || !workflow.steps) {
151
+ return res.status(400).json({ error: "Invalid workflow format" });
152
+ }
153
+ const navigator = makeNavigatorFromRequest(req);
154
+ await navigator.ensureModelReady();
155
+ console.log("Starting workflow execution...");
156
+ const runId = Date.now().toString();
157
+ const control = { aborted: false };
158
+ activeRuns.set(runId, { control });
159
+ let run;
160
+ try {
161
+ run = await navigator.runWorkflow(workflow, undefined, { runId }, control);
162
+ }
163
+ finally {
164
+ activeRuns.delete(runId);
165
+ }
166
+ // Check if results are empty and there were errors
167
+ if (run.results.length === 0 && workflow.steps && workflow.steps.length > 0) {
168
+ return res.status(500).json({
169
+ error: "Workflow execution failed to produce results. Check server logs for details.",
170
+ results: [],
171
+ run
172
+ });
173
+ }
174
+ res.json({ run });
175
+ }
176
+ catch (e) {
177
+ console.error("Server Error:", e);
178
+ res.status(500).json({ error: String(e) });
179
+ }
180
+ });
181
+ app.post('/run/:runId/stop', (req, res) => {
182
+ const runId = String(req.params.runId || '');
183
+ const active = activeRuns.get(runId);
184
+ if (!active) {
185
+ return res.status(404).json({ error: "Run not found or already finished." });
186
+ }
187
+ active.control.aborted = true;
188
+ res.json({ ok: true, runId });
189
+ });
190
+ app.post('/suite/stream', async (req, res) => {
191
+ res.setHeader('Content-Type', 'text/event-stream');
192
+ res.setHeader('Cache-Control', 'no-cache');
193
+ res.setHeader('Connection', 'keep-alive');
194
+ try {
195
+ if (!enforceGroqPlatformLimit(req, res))
196
+ return;
197
+ let suite = req.body;
198
+ const contentType = req.get('Content-Type');
199
+ if (contentType && (contentType.includes('yaml') || contentType.includes('yml'))) {
200
+ if (typeof req.body === 'string') {
201
+ try {
202
+ suite = js_yaml_1.default.load(req.body);
203
+ }
204
+ catch (e) {
205
+ res.write(`data: ${JSON.stringify({ error: "Invalid YAML format: " + String(e) })}\n\n`);
206
+ return res.end();
207
+ }
208
+ }
209
+ }
210
+ const tests = suite?.tests || suite?.workflows || suite?.suite?.tests;
211
+ if (!tests || !Array.isArray(tests) || tests.length === 0) {
212
+ res.write(`data: ${JSON.stringify({ error: "Invalid suite format (expected tests: [...])" })}\n\n`);
213
+ return res.end();
214
+ }
215
+ const suiteName = suite?.name || suite?.suite?.name || "Unnamed Suite";
216
+ res.write(`data: ${JSON.stringify({ startedSuite: true, suiteName })}\n\n`);
217
+ const navigator = makeNavigatorFromRequest(req);
218
+ await navigator.ensureModelReady();
219
+ const suiteResults = [];
220
+ for (let i = 0; i < tests.length; i++) {
221
+ const t = tests[i];
222
+ const testName = t?.name || t?.id || t?.title || "Unnamed Test";
223
+ let workflow = t?.workflow || t?.test || t;
224
+ if (typeof workflow === 'string') {
225
+ workflow = js_yaml_1.default.load(workflow);
226
+ }
227
+ else if (t?.workflow_file) {
228
+ const filePath = path_1.default.resolve(process.cwd(), String(t.workflow_file));
229
+ if (!fs_1.default.existsSync(filePath)) {
230
+ const entry = { testName, status: "failed", error: `workflow_file not found: ${t.workflow_file}` };
231
+ suiteResults.push(entry);
232
+ res.write(`data: ${JSON.stringify({ doneTest: true, ...entry })}\n\n`);
233
+ continue;
234
+ }
235
+ const fileContent = fs_1.default.readFileSync(filePath, 'utf-8');
236
+ workflow = js_yaml_1.default.load(fileContent);
237
+ }
238
+ else if (t?.steps && Array.isArray(t.steps)) {
239
+ workflow = { steps: t.steps };
240
+ }
241
+ if (!workflow || !workflow.steps) {
242
+ const entry = { testName, status: "failed", error: "Invalid workflow format in test" };
243
+ suiteResults.push(entry);
244
+ res.write(`data: ${JSON.stringify({ doneTest: true, ...entry })}\n\n`);
245
+ continue;
246
+ }
247
+ const runId = `${Date.now()}_${i + 1}`;
248
+ const control = { aborted: false };
249
+ activeRuns.set(runId, { control });
250
+ res.write(`data: ${JSON.stringify({ startedTest: true, suiteName, testName, runId })}\n\n`);
251
+ try {
252
+ const run = await navigator.runWorkflow(workflow, (stepResult) => {
253
+ res.write(`data: ${JSON.stringify({ step: stepResult, suiteName, testName, runId })}\n\n`);
254
+ }, { suiteName, testName, runId }, control);
255
+ const entry = { testName, runId: run.runId, status: run.status, issues: run.issues?.length || 0 };
256
+ suiteResults.push(entry);
257
+ res.write(`data: ${JSON.stringify({ doneTest: true, ...entry })}\n\n`);
258
+ }
259
+ finally {
260
+ activeRuns.delete(runId);
261
+ }
262
+ }
263
+ res.write(`data: ${JSON.stringify({ done: true, suiteName, results: suiteResults })}\n\n`);
264
+ res.end();
265
+ }
266
+ catch (e) {
267
+ res.write(`data: ${JSON.stringify({ error: String(e) })}\n\n`);
268
+ res.end();
269
+ }
270
+ });
271
+ app.post('/suite', async (req, res) => {
272
+ try {
273
+ if (!enforceGroqPlatformLimit(req, res))
274
+ return;
275
+ let suite = req.body;
276
+ const contentType = req.get('Content-Type');
277
+ if (contentType && (contentType.includes('yaml') || contentType.includes('yml'))) {
278
+ if (typeof req.body === 'string') {
279
+ try {
280
+ suite = js_yaml_1.default.load(req.body);
281
+ }
282
+ catch (e) {
283
+ return res.status(400).json({ error: "Invalid YAML format: " + String(e) });
284
+ }
285
+ }
286
+ }
287
+ const tests = suite?.tests || suite?.workflows || suite?.suite?.tests;
288
+ if (!tests || !Array.isArray(tests) || tests.length === 0) {
289
+ return res.status(400).json({ error: "Invalid suite format (expected tests: [...])" });
290
+ }
291
+ const suiteName = suite?.name || suite?.suite?.name || "Unnamed Suite";
292
+ const navigator = makeNavigatorFromRequest(req);
293
+ await navigator.ensureModelReady();
294
+ const suiteResults = [];
295
+ for (let i = 0; i < tests.length; i++) {
296
+ const t = tests[i];
297
+ const testName = t?.name || t?.id || t?.title || "Unnamed Test";
298
+ let workflow = t?.workflow || t?.test || t;
299
+ if (typeof workflow === 'string') {
300
+ workflow = js_yaml_1.default.load(workflow);
301
+ }
302
+ else if (t?.workflow_file) {
303
+ const filePath = path_1.default.resolve(process.cwd(), String(t.workflow_file));
304
+ if (!fs_1.default.existsSync(filePath)) {
305
+ suiteResults.push({ testName, status: "failed", error: `workflow_file not found: ${t.workflow_file}` });
306
+ continue;
307
+ }
308
+ const fileContent = fs_1.default.readFileSync(filePath, 'utf-8');
309
+ workflow = js_yaml_1.default.load(fileContent);
310
+ }
311
+ else if (t?.steps && Array.isArray(t.steps)) {
312
+ workflow = { steps: t.steps };
313
+ }
314
+ if (!workflow || !workflow.steps) {
315
+ suiteResults.push({ testName, status: "failed", error: "Invalid workflow format in test" });
316
+ continue;
317
+ }
318
+ console.log(`Starting suite test: ${suiteName} / ${testName}`);
319
+ const runId = `${Date.now()}_${i + 1}`;
320
+ const control = { aborted: false };
321
+ activeRuns.set(runId, { control });
322
+ try {
323
+ const run = await navigator.runWorkflow(workflow, undefined, { suiteName, testName, runId }, control);
324
+ suiteResults.push({ testName, runId: run.runId, status: run.status, issues: run.issues?.length || 0 });
325
+ }
326
+ finally {
327
+ activeRuns.delete(runId);
328
+ }
329
+ }
330
+ res.json({ suiteName, results: suiteResults });
331
+ }
332
+ catch (e) {
333
+ console.error("Suite Error:", e);
334
+ res.status(500).json({ error: String(e) });
335
+ }
336
+ });
337
+ app.get('/health', (req, res) => {
338
+ res.json({ status: "ok" });
339
+ });
340
+ app.get(/^\/artifacts\/(.+)$/, async (req, res) => {
341
+ const objectName = String(req.params[0] || '').trim();
342
+ if (!objectName)
343
+ return res.status(400).json({ error: "Missing object name" });
344
+ try {
345
+ const ext = path_1.default.extname(objectName).toLowerCase();
346
+ const contentType = ext === '.png'
347
+ ? 'image/png'
348
+ : ext === '.jpg' || ext === '.jpeg'
349
+ ? 'image/jpeg'
350
+ : ext === '.gif'
351
+ ? 'image/gif'
352
+ : ext === '.webp'
353
+ ? 'image/webp'
354
+ : ext === '.json'
355
+ ? 'application/json; charset=utf-8'
356
+ : ext === '.html'
357
+ ? 'text/html; charset=utf-8'
358
+ : ext === '.txt' || ext === '.log'
359
+ ? 'text/plain; charset=utf-8'
360
+ : 'application/octet-stream';
361
+ res.setHeader('Content-Type', contentType);
362
+ res.setHeader('Cache-Control', 'no-store');
363
+ const stream = await storage.getObjectStream(objectName);
364
+ stream.on('error', (e) => {
365
+ if (!res.headersSent)
366
+ res.status(404);
367
+ res.end();
368
+ });
369
+ stream.pipe(res);
370
+ }
371
+ catch (e) {
372
+ res.status(404).json({ error: "Not found" });
373
+ }
374
+ });
375
+ app.get('/model/config', (req, res) => {
376
+ const hasGroqPlatformKey = Boolean(process.env.GROQ_API_KEY) || String(process.env.OPENROUTER_API_KEY || '').startsWith('gsk_');
377
+ const hasOpenRouter = Boolean(process.env.OPENROUTER_API_KEY) && !String(process.env.OPENROUTER_API_KEY || '').startsWith('gsk_');
378
+ let providerDefault = 'ollama';
379
+ if (hasGroqPlatformKey)
380
+ providerDefault = 'groq';
381
+ else if (hasOpenRouter)
382
+ providerDefault = 'openrouter';
383
+ const modelDefault = providerDefault === 'groq'
384
+ ? (process.env.GROQ_MODEL || 'llama-3.1-8b-instant')
385
+ : providerDefault === 'openrouter'
386
+ ? (process.env.OPENROUTER_MODEL || 'google/gemini-2.0-flash-exp:free')
387
+ : (process.env.MODEL || 'qwen2.5:3b');
388
+ const usageKey = getClientKey(req);
389
+ const date = currentDateKey();
390
+ const usage = groqPlatformUsage.get(usageKey);
391
+ const usageCount = usage && usage.date === date ? usage.count : 0;
392
+ res.json({
393
+ providerDefault,
394
+ modelDefault,
395
+ groqDailyLimit,
396
+ groqUsageCount: usageCount,
397
+ byoSupported: true
398
+ });
399
+ });
400
+ function startServer(port = 8000) {
401
+ app.listen(port, () => {
402
+ console.log(`Vision Navigator Web UI running at http://localhost:${port}`);
403
+ });
404
+ }
405
+ // Auto-start if run directly (for backward compatibility during dev)
406
+ if (require.main === module) {
407
+ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8000;
408
+ startServer(PORT);
409
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Storage = void 0;
40
+ require("dotenv/config");
41
+ const pocketbase_1 = __importDefault(require("pocketbase"));
42
+ const Minio = __importStar(require("minio"));
43
+ class Storage {
44
+ constructor() {
45
+ const pbUrl = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
46
+ this.pb = new pocketbase_1.default(pbUrl);
47
+ const minioEndpoint = process.env.MINIO_ENDPOINT || '127.0.0.1';
48
+ const minioPort = parseInt(process.env.MINIO_PORT || '9000', 10);
49
+ const minioAccessKey = process.env.MINIO_ACCESS_KEY || 'minioadmin';
50
+ const minioSecretKey = process.env.MINIO_SECRET_KEY || 'minioadmin';
51
+ this.bucketName = process.env.MINIO_BUCKET || 'store-runs';
52
+ this.minioClient = new Minio.Client({
53
+ endPoint: minioEndpoint,
54
+ port: minioPort,
55
+ useSSL: false,
56
+ accessKey: minioAccessKey,
57
+ secretKey: minioSecretKey
58
+ });
59
+ this.init().catch(e => console.error("Storage init error:", e));
60
+ }
61
+ async init() {
62
+ try {
63
+ const exists = await this.minioClient.bucketExists(this.bucketName);
64
+ if (!exists) {
65
+ await this.minioClient.makeBucket(this.bucketName, 'us-east-1');
66
+ // Make bucket public so dashboard can read images directly
67
+ const policy = {
68
+ Version: '2012-10-17',
69
+ Statement: [
70
+ {
71
+ Action: ['s3:GetObject'],
72
+ Effect: 'Allow',
73
+ Principal: { AWS: ['*'] },
74
+ Resource: [`arn:aws:s3:::${this.bucketName}/*`]
75
+ }
76
+ ]
77
+ };
78
+ await this.minioClient.setBucketPolicy(this.bucketName, JSON.stringify(policy));
79
+ console.log(`Created Minio bucket: ${this.bucketName}`);
80
+ }
81
+ }
82
+ catch (e) {
83
+ console.error(`Failed to initialize Minio bucket ${this.bucketName}:`, e);
84
+ }
85
+ }
86
+ async uploadFile(filePath, objectName) {
87
+ try {
88
+ await this.minioClient.fPutObject(this.bucketName, objectName, filePath);
89
+ return objectName;
90
+ }
91
+ catch (e) {
92
+ console.error(`Minio upload failed for ${objectName}:`, e);
93
+ throw e;
94
+ }
95
+ }
96
+ async getObjectStream(objectName) {
97
+ return await this.minioClient.getObject(this.bucketName, objectName);
98
+ }
99
+ async createRun(runId, status, images, report, issues, meta) {
100
+ try {
101
+ // First check if collection exists or try to authenticate if needed.
102
+ // Assuming the runs collection exists and is open for create.
103
+ const record = await this.pb.collection('runs').create({
104
+ runId: runId, // if runId field exists, or we just rely on pb's own id
105
+ status: status,
106
+ images: images,
107
+ report: report,
108
+ issues: issues || [],
109
+ suiteName: meta?.suiteName,
110
+ testName: meta?.testName
111
+ });
112
+ console.log(`Saved run to PocketBase: ${record.id}`);
113
+ return record;
114
+ }
115
+ catch (e) {
116
+ console.error(`PocketBase createRun failed:`, e.message || e);
117
+ // Sometimes it's better not to fail the whole run if storage fails
118
+ return null;
119
+ }
120
+ }
121
+ async updateRun(recordId, data) {
122
+ try {
123
+ const record = await this.pb.collection('runs').update(recordId, data);
124
+ return record;
125
+ }
126
+ catch (e) {
127
+ console.error(`PocketBase updateRun failed:`, e.message || e);
128
+ return null;
129
+ }
130
+ }
131
+ }
132
+ exports.Storage = Storage;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "vision-navigator",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered browser testing and automation framework",
5
+ "main": "dist/navigator.js",
6
+ "bin": {
7
+ "vision-navigator": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "public"
12
+ ],
13
+ "scripts": {
14
+ "test": "npm run test:cli",
15
+ "test:cli": "node tests/test-cli.js",
16
+ "build": "tsc",
17
+ "start": "node dist/cli.js",
18
+ "dev": "nodemon src/server.ts",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "ai",
23
+ "browser",
24
+ "testing",
25
+ "automation",
26
+ "cdp",
27
+ "vision"
28
+ ],
29
+ "author": "David Soderman",
30
+ "license": "MIT",
31
+ "type": "commonjs",
32
+ "dependencies": {
33
+ "@types/express": "^5.0.6",
34
+ "@types/js-yaml": "^4.0.9",
35
+ "@types/node": "^25.4.0",
36
+ "@types/ws": "^8.18.1",
37
+ "axios": "^1.13.6",
38
+ "dotenv": "^17.3.1",
39
+ "express": "^5.2.1",
40
+ "js-yaml": "^4.1.1",
41
+ "minio": "^8.0.7",
42
+ "nodemon": "^3.1.14",
43
+ "pocketbase": "^0.26.8",
44
+ "ts-node": "^10.9.2",
45
+ "typescript": "^5.9.3",
46
+ "ws": "^8.19.0"
47
+ }
48
+ }