jevway 0.2.0 → 0.4.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/README.md +37 -2
- package/{INSTALL-FOR-AGENTS.md → SETUP.md} +94 -14
- package/dist/browser/code-boxes.js +121 -0
- package/dist/browser/downloads.js +103 -0
- package/dist/browser/frames.js +23 -2
- package/dist/browser/snapshot.js +82 -1
- package/dist/browser/tab.js +100 -12
- package/dist/cli/dispatch.js +2 -0
- package/dist/cli/inbox-command.js +199 -0
- package/dist/cli/look-command.js +10 -3
- package/dist/cli/run-command.js +51 -3
- package/dist/daemon/server.js +22 -3
- package/dist/inbox/inbox-file.js +43 -0
- package/dist/inbox/inbox-registry.js +33 -0
- package/dist/inbox/inbox.js +127 -0
- package/dist/inbox/mail-api.js +52 -0
- package/dist/inbox/message.js +34 -0
- package/dist/jev/secrets.js +4 -3
- package/dist/mcp/inbox-tools.js +82 -0
- package/dist/mcp/instructions.js +11 -2
- package/dist/mcp/server.js +15 -8
- package/dist/mcp/task-tools.js +24 -8
- package/dist/notify/desktop.js +40 -0
- package/dist/notify/person-messages.js +50 -0
- package/dist/output/layout.js +37 -1
- package/dist/output/manifest.js +6 -0
- package/dist/output/run-folder.js +44 -2
- package/dist/recording/describe-step.js +3 -0
- package/dist/session/session-service.js +31 -11
- package/dist/setup/skill-command.js +44 -21
- package/dist/task/ask-user.js +182 -0
- package/dist/task/downloads.js +63 -0
- package/dist/task/person-wait.js +104 -0
- package/dist/task/present.js +32 -11
- package/dist/task/run-task.js +327 -36
- package/dist/task/start-task.js +49 -6
- package/dist/task/totp.js +124 -0
- package/dist/task/upload-files.js +237 -5
- package/package.json +2 -2
- package/skill/jevway/SKILL.md +5 -2
- package/skill/jevway-inbox/SKILL.md +61 -0
- package/skill/jevway-inbox/scripts/inbox.mjs +209 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Identity providers' sign-in hosts. Their own pages (sign-up, settings) carry no request from another site.
|
|
2
|
+
const SSO_HOSTS = {
|
|
3
|
+
'accounts.google.com': 'Google',
|
|
4
|
+
'appleid.apple.com': 'Apple',
|
|
5
|
+
'login.microsoftonline.com': 'Microsoft',
|
|
6
|
+
'login.live.com': 'Microsoft',
|
|
7
|
+
};
|
|
8
|
+
// A path segment that is a step of signing in: a page there is still the sign-in, whatever it shows.
|
|
9
|
+
const SIGN_IN_STEP = /^(log-?in|sign-?in|sign_in|sessions?|auth|oauth2?|sso|saml2?|2fa|two-factor|mfa|otp|verify|challenge)$/i;
|
|
10
|
+
// A query decoded up to twice: Google nests the OAuth request URL-encoded inside `continue`.
|
|
11
|
+
const decodedQuery = (search) => {
|
|
12
|
+
let query = search;
|
|
13
|
+
for (let i = 0; i < 2; i++) {
|
|
14
|
+
try {
|
|
15
|
+
query = decodeURIComponent(query);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return query;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The identity provider when `url` is its sign-in for another site: a provider host whose query carries an OAuth or
|
|
25
|
+
* OIDC request (`client_id` and `redirect_uri`, possibly nested in `continue`) or a SAML one (`SAMLRequest`).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ssoProviderOf('https://accounts.google.com/o/oauth2/v2/auth?client_id=1&redirect_uri=https%3A%2F%2Fapp.example') // 'Google'
|
|
29
|
+
* ssoProviderOf('https://accounts.google.com/signup') // undefined
|
|
30
|
+
*/
|
|
31
|
+
export const ssoProviderOf = (url) => {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = new URL(url);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const provider = SSO_HOSTS[parsed.hostname];
|
|
40
|
+
if (!provider)
|
|
41
|
+
return undefined;
|
|
42
|
+
const query = decodedQuery(parsed.search);
|
|
43
|
+
const oauth = /[?&]client_id=/.test(query) && /[?&]redirect_uri=/.test(query);
|
|
44
|
+
return oauth || /[?&]SAMLRequest=/.test(query) ? provider : undefined;
|
|
45
|
+
};
|
|
46
|
+
const pathOf = (url) => {
|
|
47
|
+
try {
|
|
48
|
+
const { origin, pathname } = new URL(url);
|
|
49
|
+
return { at: `${origin}${pathname}`, segments: pathname.split('/').filter(Boolean) };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { at: url, segments: [] };
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Whether `page` is still the sign-in `wall` was on: it shows a password field (typed or not), it is the wall's page
|
|
57
|
+
* still listing a field the wall had empty (filled or not), its path is a sign-in step (`/login`, `/sessions/two-factor`,
|
|
58
|
+
* `/auth/callback`), or it is an SSO page. Conservative: a person mid-way is never taken for done.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* stillSigningIn(dashboard, loginPage) // false
|
|
62
|
+
*/
|
|
63
|
+
export const stillSigningIn = (page, wall) => {
|
|
64
|
+
if (page.passwordFields?.length)
|
|
65
|
+
return true;
|
|
66
|
+
const here = pathOf(page.url);
|
|
67
|
+
if (here.at === pathOf(wall.url).at) {
|
|
68
|
+
const wallFields = new Set(wall.actions.filter((action) => action.kind === 'fill' && !action.value).map((action) => action.label));
|
|
69
|
+
if (page.actions.some((action) => action.kind === 'fill' && wallFields.has(action.label)))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return here.segments.some((segment) => SIGN_IN_STEP.test(segment)) || Boolean(ssoProviderOf(page.url));
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Reads the page until the wall is gone for `clearReads` reads in a row, the wait runs out, or the call is stopped.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const { outcome, page } = await waitForPersonLoop({ waitMs: 120_000, pollMs: 1000, tickMs: 10_000, clearReads: 1, read, isWall, pause, stopped, tell });
|
|
79
|
+
*/
|
|
80
|
+
export const waitForPersonLoop = async ({ waitMs, pollMs, tickMs, clearReads, read, isWall, pause, stopped, tell }) => {
|
|
81
|
+
const deadline = performance.now() + waitMs;
|
|
82
|
+
let nextTick = performance.now();
|
|
83
|
+
let clear = 0;
|
|
84
|
+
let page;
|
|
85
|
+
for (;;) {
|
|
86
|
+
const now = performance.now();
|
|
87
|
+
if (now >= deadline)
|
|
88
|
+
return { outcome: 'ran_out', page };
|
|
89
|
+
if (now >= nextTick) {
|
|
90
|
+
tell(Math.ceil((deadline - now) / 1000));
|
|
91
|
+
nextTick = now + tickMs;
|
|
92
|
+
}
|
|
93
|
+
await pause(Math.min(pollMs, deadline - now));
|
|
94
|
+
if (stopped())
|
|
95
|
+
return { outcome: 'stopped', page };
|
|
96
|
+
const next = await read();
|
|
97
|
+
if (!next)
|
|
98
|
+
return { outcome: 'gone', page };
|
|
99
|
+
page = next;
|
|
100
|
+
clear = (await isWall(page)) ? 0 : clear + 1;
|
|
101
|
+
if (clear >= clearReads)
|
|
102
|
+
return { outcome: 'passed', page };
|
|
103
|
+
}
|
|
104
|
+
};
|
package/dist/task/present.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describeStep } from '../recording/describe-step.js';
|
|
2
|
+
import { askUserFor } from './ask-user.js';
|
|
2
3
|
// What the run appends to a step's element, in parentheses: a tab switch, a dialog, a scroll that moved nothing.
|
|
3
|
-
const NOTE = / \((?=new tab opened: |the tab closed; |nothing moved: |the page then )/;
|
|
4
|
+
const NOTE = / \((?=new tab opened: |the tab closed; |nothing moved: |the page then |downloaded |download of )/;
|
|
4
5
|
const splitNotes = (element) => {
|
|
5
6
|
const [label, ...notes] = element.split(NOTE);
|
|
6
7
|
return { label, notes: notes.map((note) => note.replace(/\)$/, '')) };
|
|
@@ -10,6 +11,10 @@ const outcomeOf = (note) => {
|
|
|
10
11
|
return `a ${note}`;
|
|
11
12
|
if (note.startsWith('nothing moved: '))
|
|
12
13
|
return 'nothing moved';
|
|
14
|
+
// 'downloaded invoice.pdf, 84 KB' → 'downloaded invoice.pdf (84 KB)'
|
|
15
|
+
const downloaded = /^(downloaded .+), (\d[\d.]* (?:bytes|KB|MB))$/.exec(note);
|
|
16
|
+
if (downloaded)
|
|
17
|
+
return `${downloaded[1]} (${downloaded[2]})`;
|
|
13
18
|
return note;
|
|
14
19
|
};
|
|
15
20
|
const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
@@ -38,40 +43,56 @@ export const storyLine = (step) => {
|
|
|
38
43
|
};
|
|
39
44
|
/** A call's steps as one sentence each. */
|
|
40
45
|
export const storyOf = (steps) => steps.map(storyLine);
|
|
46
|
+
const ASK = 'Ask the user the `ask_user` question with your ask tool';
|
|
41
47
|
/**
|
|
42
|
-
* The one thing the caller does next after this status, in the tools' parameter names
|
|
48
|
+
* The one thing the caller does next after this status, in the tools' parameter names; with `asking`, it starts from
|
|
49
|
+
* the result's `ask_user` question.
|
|
43
50
|
*
|
|
44
51
|
* @example
|
|
45
52
|
* nextFor({ status: 'needs_input', … }) // 'Add the missing values to `values` and call again with this session.'
|
|
46
53
|
*/
|
|
47
|
-
export const nextFor = ({ status, wall }) => {
|
|
54
|
+
export const nextFor = ({ status, wall }, { asking = false } = {}) => {
|
|
48
55
|
switch (status) {
|
|
49
56
|
case 'done':
|
|
50
57
|
return 'Read the result in `page_text` (and `evidence`); end the session when you no longer need its tab.';
|
|
51
58
|
case 'needs_input':
|
|
52
|
-
return
|
|
59
|
+
return asking
|
|
60
|
+
? `${ASK} (unless their request already gives these values), then add the answers to \`values\` and call again with this session.`
|
|
61
|
+
: 'Add the missing values to `values` and call again with this session.';
|
|
53
62
|
case 'needs_confirmation':
|
|
54
|
-
return
|
|
63
|
+
return asking
|
|
64
|
+
? `${ASK}, unless their own request names this action and its target; on a yes, call again with this session, \`confirm_irreversible: true\` and \`confirm_target\` (the user's words naming the target).`
|
|
65
|
+
: "Ask the user, unless their own request names this action and its target; then call again with this session, `confirm_irreversible: true` and `confirm_target` (the user's words naming the target).";
|
|
55
66
|
case 'uncertain':
|
|
56
|
-
return
|
|
67
|
+
return asking
|
|
68
|
+
? `${ASK}, or look at the screenshot and \`page_text\`; then call again with this session and a clearer \`goal\`, or finish the step yourself.`
|
|
69
|
+
: 'Look at the screenshot and `page_text`, then call again with this session and a clearer `goal`, or finish the step yourself.';
|
|
57
70
|
case 'step_budget':
|
|
58
71
|
return 'Call again with this session and a narrower `goal`, or a larger `max_steps` / `max_seconds`.';
|
|
59
72
|
case 'blocked':
|
|
60
|
-
if (wall?.kind === 'bot_check')
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
if (wall?.kind === 'bot_check') {
|
|
74
|
+
return asking
|
|
75
|
+
? `Never try to get past it: ${ASK.charAt(0).toLowerCase()}${ASK.slice(1)} (they pass it in that tab), then call again with this session.`
|
|
76
|
+
: 'Never try to get past it: ask the user to pass it in that tab, then call again with this session.';
|
|
77
|
+
}
|
|
78
|
+
if (wall?.kind === 'sign_in') {
|
|
79
|
+
return asking
|
|
80
|
+
? `${ASK} (they sign in at ${wall.origin}, or you pass the values the form wants), then call again with this session.`
|
|
81
|
+
: `Sign in at ${wall.origin} (or pass the values the form wants), then call again with this session.`;
|
|
82
|
+
}
|
|
64
83
|
return 'Read the reason: tell the user, or change `goal` or `values` and call again with this session.';
|
|
65
84
|
}
|
|
66
85
|
};
|
|
67
86
|
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, inner]) => inner !== undefined));
|
|
68
87
|
export function presentSummary({ session, summary, files }, { detail = 'concise' } = {}) {
|
|
88
|
+
const ask_user = askUserFor(summary);
|
|
69
89
|
// The order both adapters print: how it ended and what to do first, then where, and the long page text last.
|
|
70
90
|
const { page_text, ...head } = {
|
|
71
91
|
status: summary.status,
|
|
72
92
|
reason: summary.reason,
|
|
73
93
|
story: storyOf(summary.steps),
|
|
74
|
-
next: nextFor(summary),
|
|
94
|
+
next: nextFor(summary, { asking: ask_user !== undefined }),
|
|
95
|
+
ask_user,
|
|
75
96
|
session,
|
|
76
97
|
url: summary.url,
|
|
77
98
|
title: summary.title,
|