jevway 0.0.0-stage → 0.1.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.
Files changed (43) hide show
  1. package/INSTALL-FOR-AGENTS.md +138 -0
  2. package/LICENSE +11 -0
  3. package/LICENSE.jev-ultrafast +21 -0
  4. package/README.md +218 -2
  5. package/dist/browser/bot-wall.js +105 -0
  6. package/dist/browser/cdp.js +112 -0
  7. package/dist/browser/chrome.js +147 -0
  8. package/dist/browser/devices.js +20 -0
  9. package/dist/browser/frames.js +516 -0
  10. package/dist/browser/snapshot.js +611 -0
  11. package/dist/browser/tab.js +938 -0
  12. package/dist/browser/words.js +51 -0
  13. package/dist/cli.js +112 -0
  14. package/dist/jev/action-space.js +48 -0
  15. package/dist/jev/box.js +58 -0
  16. package/dist/jev/decide.js +405 -0
  17. package/dist/jev/instructions.js +67 -0
  18. package/dist/jev/model.js +189 -0
  19. package/dist/jev/replay.js +213 -0
  20. package/dist/jev/secrets.js +60 -0
  21. package/dist/journeys/diagnostics.js +85 -0
  22. package/dist/journeys/journey-doc.js +50 -0
  23. package/dist/journeys/journey-session.js +503 -0
  24. package/dist/journeys/pick-next-move.js +59 -0
  25. package/dist/journeys/render-viewer.js +153 -0
  26. package/dist/journeys/screen-boxes.js +49 -0
  27. package/dist/journeys/screen-judgments.js +149 -0
  28. package/dist/mcp.js +361 -0
  29. package/dist/setup/doctor-command.js +81 -0
  30. package/dist/setup/doctor.js +163 -0
  31. package/dist/setup/env-file.js +50 -0
  32. package/dist/task/checks.js +156 -0
  33. package/dist/task/evidence.js +96 -0
  34. package/dist/task/hosts.js +91 -0
  35. package/dist/task/look.js +17 -0
  36. package/dist/task/recording-page.js +310 -0
  37. package/dist/task/recording.js +232 -0
  38. package/dist/task/run-limit.js +66 -0
  39. package/dist/task/run-task.js +1607 -0
  40. package/dist/task/start-task.js +91 -0
  41. package/dist/task/upload-files.js +43 -0
  42. package/package.json +80 -4
  43. package/skill/jevway/SKILL.md +73 -0
@@ -0,0 +1,147 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { CdpConnection } from './cdp.js';
6
+ export const connectChrome = async (source) => {
7
+ if (source === 'launch' || source === 'launch-headless')
8
+ return launchChrome(source === 'launch-headless');
9
+ const endpoint = source === 'auto' ? autoEndpoint() : await explicitEndpoint(source);
10
+ const cdp = await CdpConnection.open(endpoint);
11
+ return { cdp, endpoint, close: async () => cdp.close() };
12
+ };
13
+ /**
14
+ * The port and browser path Chrome writes to `DevToolsActivePort` while remote debugging is allowed
15
+ * (chrome://inspect/#remote-debugging). The file can outlive Chrome, so a port here may no longer answer.
16
+ *
17
+ * @example
18
+ * devToolsActivePort() // { port: '9222', path: '/devtools/browser/…', file: '…/Google/Chrome/DevToolsActivePort' }
19
+ */
20
+ export const devToolsActivePort = () => {
21
+ const file = chromeProfileDirs()
22
+ .map((dir) => join(dir, 'DevToolsActivePort'))
23
+ .find(existsSync);
24
+ if (!file)
25
+ return undefined;
26
+ const [port, path] = readFileSync(file, 'utf8').trim().split('\n');
27
+ return { port, path, file };
28
+ };
29
+ const autoEndpoint = () => {
30
+ const active = devToolsActivePort();
31
+ if (!active) {
32
+ throw new Error('No running Chrome with remote debugging. Open chrome://inspect/#remote-debugging and allow it.');
33
+ }
34
+ return `ws://127.0.0.1:${active.port}${active.path}`;
35
+ };
36
+ const chromeProfileDirs = () => {
37
+ const home = homedir();
38
+ if (process.platform === 'darwin')
39
+ return [join(home, 'Library/Application Support/Google/Chrome')];
40
+ if (process.platform === 'win32')
41
+ return [join(process.env.LOCALAPPDATA ?? '', 'Google/Chrome/User Data')];
42
+ return [join(home, '.config/google-chrome'), join(home, '.config/chromium')];
43
+ };
44
+ const explicitEndpoint = async (url) => {
45
+ if (url.startsWith('ws'))
46
+ return url;
47
+ const version = await fetch(new URL('/json/version', url)).then((response) => response.json());
48
+ return version.webSocketDebuggerUrl;
49
+ };
50
+ /** The Chrome `launch` starts: `CHROME_PATH`, else the usual install path for this OS; undefined when none exists. */
51
+ export const findChrome = () => [
52
+ process.env.CHROME_PATH,
53
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
54
+ '/usr/bin/google-chrome',
55
+ '/usr/bin/chromium',
56
+ 'C:/Program Files/Google/Chrome/Application/chrome.exe',
57
+ ].find((path) => path && existsSync(path));
58
+ const chromeExecutable = () => {
59
+ const found = findChrome();
60
+ if (!found)
61
+ throw new Error('Chrome not found. Set CHROME_PATH.');
62
+ return found;
63
+ };
64
+ const launchChrome = async (headless) => {
65
+ const profile = mkdtempSync(join(tmpdir(), 'jevway-'));
66
+ const args = [
67
+ `--user-data-dir=${profile}`,
68
+ '--remote-debugging-port=0',
69
+ '--no-first-run',
70
+ '--no-default-browser-check',
71
+ '--window-size=1120,780',
72
+ ...(headless ? ['--headless=new'] : []),
73
+ 'about:blank',
74
+ ];
75
+ const chrome = spawn(chromeExecutable(), args, { stdio: ['ignore', 'ignore', 'pipe'] });
76
+ // Any failure before a handle is returned must not leave Chrome or its profile behind.
77
+ const abandon = async (error) => {
78
+ await stopChrome(chrome);
79
+ try {
80
+ rmSync(profile, { recursive: true, force: true });
81
+ }
82
+ catch { }
83
+ throw error;
84
+ };
85
+ const endpoint = await readDevToolsEndpoint(chrome).catch(abandon);
86
+ const cdp = await CdpConnection.open(endpoint).catch(abandon);
87
+ return {
88
+ cdp,
89
+ endpoint,
90
+ // Returns even if Chrome already exited; a profile that can't be removed never rejects.
91
+ close: async () => {
92
+ cdp.close();
93
+ await stopChrome(chrome);
94
+ try {
95
+ rmSync(profile, { recursive: true, force: true });
96
+ }
97
+ catch { }
98
+ },
99
+ };
100
+ };
101
+ /**
102
+ * Stops a launched Chrome and resolves once it is gone, never hanging: a process that already exited returns at
103
+ * once, one that ignores SIGTERM is killed after `graceMs`. Never rejects. Exported for tests.
104
+ *
105
+ * @example
106
+ * await stopChrome(child); // resolves even if Chrome crashed earlier
107
+ */
108
+ export const stopChrome = async (child, graceMs = 5_000) => {
109
+ if (child.exitCode !== null || child.signalCode !== null)
110
+ return;
111
+ // Listen before signalling, so a fast exit is never missed.
112
+ const exited = new Promise((resolve) => child.once('exit', () => resolve(true)));
113
+ child.kill();
114
+ if (await exitedWithin(exited, graceMs))
115
+ return;
116
+ child.kill('SIGKILL');
117
+ await exitedWithin(exited, 1_000);
118
+ };
119
+ // True if the process exited before `ms` passed; the timer never outlives the wait.
120
+ const exitedWithin = async (exited, ms) => {
121
+ let timer;
122
+ const timedOut = new Promise((resolve) => {
123
+ timer = setTimeout(resolve, ms, false);
124
+ });
125
+ try {
126
+ return await Promise.race([exited, timedOut]);
127
+ }
128
+ finally {
129
+ clearTimeout(timer);
130
+ }
131
+ };
132
+ const readDevToolsEndpoint = (chrome) => new Promise((resolve, reject) => {
133
+ let output = '';
134
+ const timer = setTimeout(() => reject(new Error('Chrome did not start within 15s')), 15_000);
135
+ chrome.stderr.on('data', (chunk) => {
136
+ output += chunk;
137
+ const match = output.match(/DevTools listening on (ws:\/\/\S+)/);
138
+ if (match) {
139
+ clearTimeout(timer);
140
+ resolve(match[1]);
141
+ }
142
+ });
143
+ chrome.once('exit', (code) => {
144
+ clearTimeout(timer);
145
+ reject(new Error(`Chrome exited (${code}) before DevTools started`));
146
+ });
147
+ });
@@ -0,0 +1,20 @@
1
+ export const DEVICES = {
2
+ desktop: { name: 'desktop', width: 1120, height: 780, deviceScaleFactor: 1, mobile: false },
3
+ laptop: { name: 'laptop', width: 1440, height: 900, deviceScaleFactor: 1, mobile: false },
4
+ 'iphone-15': {
5
+ name: 'iphone-15',
6
+ width: 393,
7
+ height: 852,
8
+ deviceScaleFactor: 2,
9
+ mobile: true,
10
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
11
+ },
12
+ 'pixel-8': {
13
+ name: 'pixel-8',
14
+ width: 412,
15
+ height: 915,
16
+ deviceScaleFactor: 2,
17
+ mobile: true,
18
+ userAgent: 'Mozilla/5.0 (Linux; Android 15; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Mobile Safari/537.36',
19
+ },
20
+ };