portal-agent-cli 1.0.1 → 3.0.1
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/LICENSE +2 -18
- package/README.md +36 -86
- package/lib/args.mjs +62 -26
- package/lib/browser.mjs +31 -9
- package/lib/config.mjs +77 -0
- package/lib/deepthink.mjs +55 -0
- package/lib/exec.mjs +1 -25
- package/lib/instructions.mjs +1 -0
- package/lib/log.mjs +41 -0
- package/lib/parse.mjs +18 -59
- package/lib/paste.mjs +5 -3
- package/lib/providers.mjs +35 -9
- package/lib/repl.mjs +1 -116
- package/lib/safety.mjs +1 -0
- package/lib/send.mjs +68 -48
- package/lib/sessions.mjs +1 -0
- package/lib/system.mjs +3 -23
- package/lib/theme.mjs +49 -0
- package/lib/tools/delete.mjs +2 -0
- package/lib/tools/edit.mjs +1 -0
- package/lib/tools/find.mjs +1 -0
- package/lib/tools/git.mjs +5 -0
- package/lib/tools/grep.mjs +2 -0
- package/lib/tools/index.mjs +2 -0
- package/lib/tools/list.mjs +1 -0
- package/lib/tools/path.mjs +1 -0
- package/lib/tools/read.mjs +4 -0
- package/lib/tools/shell.mjs +5 -0
- package/lib/tools/walk.mjs +1 -0
- package/lib/tools/write.mjs +5 -0
- package/lib/turn.mjs +1 -56
- package/lib/wait.mjs +44 -13
- package/package.json +6 -9
- package/portal.mjs +26 -4
- package/lib/tools.mjs +0 -151
package/lib/paste.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { sleep } from "./wait.mjs";
|
|
2
2
|
|
|
3
|
+
// DeepSeek shows a Cancel/Send confirmation bar when a large paste is
|
|
4
|
+
// detected. Find the Cancel button, locate its Send sibling, click that.
|
|
3
5
|
export async function dismissPaste(page) {
|
|
4
6
|
const btns = page.locator("button, [role=button]");
|
|
5
7
|
let total = 0;
|
|
@@ -33,7 +35,7 @@ export async function dismissPaste(page) {
|
|
|
33
35
|
try {
|
|
34
36
|
await sib.click({ timeout: 1500 });
|
|
35
37
|
await sleep(200);
|
|
36
|
-
return;
|
|
38
|
+
return true;
|
|
37
39
|
} catch (e) {
|
|
38
40
|
// fall through
|
|
39
41
|
}
|
|
@@ -46,8 +48,9 @@ export async function dismissPaste(page) {
|
|
|
46
48
|
} catch (e) {
|
|
47
49
|
// ignore
|
|
48
50
|
}
|
|
49
|
-
return;
|
|
51
|
+
return true;
|
|
50
52
|
}
|
|
53
|
+
return false;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export async function composerEmpty(input, ms) {
|
|
@@ -57,7 +60,6 @@ export async function composerEmpty(input, ms) {
|
|
|
57
60
|
try {
|
|
58
61
|
v = await input.inputValue();
|
|
59
62
|
} catch (e) {
|
|
60
|
-
// contenteditable composers have no inputValue. Treat as sent.
|
|
61
63
|
return true;
|
|
62
64
|
}
|
|
63
65
|
if (!v || v.length === 0) return true;
|
package/lib/providers.mjs
CHANGED
|
@@ -3,19 +3,41 @@ export const PROVIDERS = {
|
|
|
3
3
|
id: "deepseek",
|
|
4
4
|
label: "DeepSeek",
|
|
5
5
|
url: "https://chat.deepseek.com/",
|
|
6
|
-
input: "textarea",
|
|
7
|
-
submit: "button[aria-label=Send]",
|
|
8
|
-
response: "div.ds-markdown",
|
|
9
|
-
paste: true
|
|
6
|
+
input: ["textarea#chat-input", "textarea", "[contenteditable=true]"],
|
|
7
|
+
submit: ["button[aria-label=Send]", "button[data-testid=send-button]"],
|
|
8
|
+
response: ["div.ds-markdown", "div[class*=message]"],
|
|
9
|
+
paste: true,
|
|
10
|
+
deepthink: true
|
|
10
11
|
},
|
|
11
12
|
chatgpt: {
|
|
12
13
|
id: "chatgpt",
|
|
13
14
|
label: "ChatGPT",
|
|
14
15
|
url: "https://chatgpt.com/",
|
|
15
|
-
input: "#prompt-textarea",
|
|
16
|
-
submit: "button[data-testid=send-button]",
|
|
17
|
-
response: "[data-message-author-role=assistant]",
|
|
18
|
-
paste: false
|
|
16
|
+
input: ["#prompt-textarea", "div[contenteditable=true][id=prompt-textarea]"],
|
|
17
|
+
submit: ["button[data-testid=send-button]", "button[aria-label*=Send]"],
|
|
18
|
+
response: ["[data-message-author-role=assistant]", "article[data-testid^=conversation-turn]"],
|
|
19
|
+
paste: false,
|
|
20
|
+
deepthink: false
|
|
21
|
+
},
|
|
22
|
+
gemini: {
|
|
23
|
+
id: "gemini",
|
|
24
|
+
label: "Gemini",
|
|
25
|
+
url: "https://gemini.google.com/app",
|
|
26
|
+
input: ["div[contenteditable=true][role=textbox]", "rich-textarea div[contenteditable=true]"],
|
|
27
|
+
submit: ["button[aria-label*=Send]", "button[aria-label=Send message]"],
|
|
28
|
+
response: ["message-content", "div[class*=model-response-text]"],
|
|
29
|
+
paste: false,
|
|
30
|
+
deepthink: false
|
|
31
|
+
},
|
|
32
|
+
grok: {
|
|
33
|
+
id: "grok",
|
|
34
|
+
label: "Grok",
|
|
35
|
+
url: "https://grok.com/",
|
|
36
|
+
input: ["textarea", "div[contenteditable=true]"],
|
|
37
|
+
submit: ["button[type=submit]", "button[aria-label*=Send]"],
|
|
38
|
+
response: ["div[class*=message]", "div[class*=response]"],
|
|
39
|
+
paste: false,
|
|
40
|
+
deepthink: false
|
|
19
41
|
}
|
|
20
42
|
};
|
|
21
43
|
|
|
@@ -23,7 +45,11 @@ export function getProvider(id) {
|
|
|
23
45
|
const p = PROVIDERS[id];
|
|
24
46
|
if (!p) {
|
|
25
47
|
const names = Object.keys(PROVIDERS).join(", ");
|
|
26
|
-
throw new Error("unknown provider: " + id + "
|
|
48
|
+
throw new Error("unknown provider: " + id + "\nAvailable: " + names);
|
|
27
49
|
}
|
|
28
50
|
return p;
|
|
29
51
|
}
|
|
52
|
+
|
|
53
|
+
export function listProviders() {
|
|
54
|
+
return Object.keys(PROVIDERS).map(function (k) { return PROVIDERS[k]; });
|
|
55
|
+
}
|
package/lib/repl.mjs
CHANGED
|
@@ -1,119 +1,4 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { launch } from "./
|
|
5
|
-
import { getProvider } from "./providers.mjs";
|
|
6
|
-
import { waitForComposer } from "./wait.mjs";
|
|
7
|
-
import { runTurn } from "./turn.mjs";
|
|
8
|
-
import { setWs, getWs } from "./tools.mjs";
|
|
9
|
-
|
|
10
|
-
export async function runRepl(opts) {
|
|
11
|
-
const provider = getProvider(opts.provider);
|
|
12
|
-
setWs(opts.ws);
|
|
13
|
-
|
|
14
|
-
console.log("");
|
|
15
|
-
console.log("Portal -- web AI in your terminal");
|
|
16
|
-
console.log("workspace: " + getWs());
|
|
17
|
-
console.log("provider: " + provider.label);
|
|
18
|
-
console.log("");
|
|
19
|
-
|
|
20
|
-
const ctx = await launch();
|
|
21
|
-
const page = await ctx.newPage();
|
|
22
|
-
page.setDefaultTimeout(30000);
|
|
23
|
-
|
|
24
|
-
console.log("opening " + provider.label + "...");
|
|
25
|
-
await page.goto(provider.url);
|
|
26
|
-
console.log("");
|
|
27
|
-
console.log("Log in to " + provider.label + " in the Chrome window.");
|
|
28
|
-
console.log("Waiting up to " + Math.round(opts.loginTimeout / 1000) + "s.");
|
|
29
|
-
console.log("");
|
|
30
|
-
|
|
31
|
-
await waitForComposer(page, provider, opts.loginTimeout);
|
|
32
|
-
|
|
33
|
-
console.log("+ " + provider.label + " ready");
|
|
34
|
-
console.log("type /help for commands");
|
|
35
|
-
console.log("");
|
|
36
|
-
|
|
37
|
-
const rl = createInterface({
|
|
38
|
-
input: process.stdin,
|
|
39
|
-
output: process.stdout,
|
|
40
|
-
prompt: "> "
|
|
41
|
-
});
|
|
42
|
-
rl.prompt();
|
|
43
|
-
|
|
44
|
-
let busy = false;
|
|
45
|
-
|
|
46
|
-
rl.on("line", async function (line) {
|
|
47
|
-
const t = line.trim();
|
|
48
|
-
|
|
49
|
-
if (busy) {
|
|
50
|
-
rl.prompt();
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (!t) {
|
|
54
|
-
rl.prompt();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (t === "/quit" || t === "/exit") {
|
|
58
|
-
rl.close();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (t === "/help") {
|
|
62
|
-
help();
|
|
63
|
-
rl.prompt();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (t === "/pwd") {
|
|
67
|
-
console.log(getWs());
|
|
68
|
-
rl.prompt();
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (t.slice(0, 4) === "/cd ") {
|
|
72
|
-
changeDir(t.slice(4).trim(), rl);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
busy = true;
|
|
77
|
-
try {
|
|
78
|
-
await runTurn(page, provider, t, opts);
|
|
79
|
-
} catch (e) {
|
|
80
|
-
console.log("x " + (e.message || e));
|
|
81
|
-
}
|
|
82
|
-
busy = false;
|
|
83
|
-
console.log("");
|
|
84
|
-
rl.prompt();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
rl.on("close", async function () {
|
|
88
|
-
console.log("");
|
|
89
|
-
console.log("closing browser...");
|
|
90
|
-
await ctx.close().catch(function () {});
|
|
91
|
-
process.exit(0);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function help() {
|
|
96
|
-
console.log(" /cd <folder> change workspace");
|
|
97
|
-
console.log(" /pwd show current workspace");
|
|
98
|
-
console.log(" /help show this help");
|
|
99
|
-
console.log(" /quit exit");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function changeDir(target, rl) {
|
|
103
|
-
if (!target) {
|
|
104
|
-
console.log("usage: /cd <folder>");
|
|
105
|
-
console.log("current: " + getWs());
|
|
106
|
-
rl.prompt();
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const clean = target.replace(/^"|"$/g, "");
|
|
110
|
-
const abs = resolve(clean);
|
|
111
|
-
if (!existsSync(abs)) {
|
|
112
|
-
console.log("not found: " + abs);
|
|
113
|
-
rl.prompt();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
setWs(abs);
|
|
117
|
-
console.log("workspace: " + getWs());
|
|
118
|
-
rl.prompt();
|
|
119
|
-
}
|
|
4
|
+
import { launch } from "./
|
package/lib/safety.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import
|
package/lib/send.mjs
CHANGED
|
@@ -1,88 +1,108 @@
|
|
|
1
|
-
import { sleep, norm } from "./wait.mjs";
|
|
1
|
+
import { sleep, norm, findFirstVisible } from "./wait.mjs";
|
|
2
2
|
import { dismissPaste, composerEmpty } from "./paste.mjs";
|
|
3
|
+
import { theme } from "./theme.mjs";
|
|
4
|
+
|
|
5
|
+
const SETTLE_MS = 1500;
|
|
6
|
+
const POLL_MS = 150;
|
|
7
|
+
|
|
8
|
+
function makeStreamer() {
|
|
9
|
+
let printed = 0;
|
|
10
|
+
let lastLength = 0;
|
|
11
|
+
return function (text) {
|
|
12
|
+
if (!text) return;
|
|
13
|
+
const t = String(text);
|
|
14
|
+
if (t.length > lastLength) {
|
|
15
|
+
const delta = t.slice(printed);
|
|
16
|
+
process.stdout.write(delta);
|
|
17
|
+
printed = t.length;
|
|
18
|
+
lastLength = t.length;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
3
22
|
|
|
4
23
|
export async function send(page, provider, prompt, timeoutMs) {
|
|
5
|
-
const input = page
|
|
6
|
-
|
|
7
|
-
|
|
24
|
+
const input = await findFirstVisible(page, provider.input, 30000);
|
|
25
|
+
if (!input) throw new Error("composer not found");
|
|
26
|
+
|
|
27
|
+
const want = norm(prompt);
|
|
8
28
|
|
|
9
29
|
await input.focus();
|
|
10
30
|
try {
|
|
11
31
|
await page.keyboard.press("Control+A");
|
|
12
32
|
await page.keyboard.press("Delete");
|
|
13
33
|
} catch (e) {
|
|
14
|
-
// composer
|
|
34
|
+
// empty composer
|
|
15
35
|
}
|
|
16
36
|
|
|
17
37
|
await page.keyboard.insertText(prompt);
|
|
18
38
|
await sleep(300);
|
|
19
|
-
|
|
20
39
|
if (provider.paste) await dismissPaste(page);
|
|
21
40
|
|
|
22
41
|
try {
|
|
23
42
|
await input.press("Enter");
|
|
24
43
|
} catch (e) {
|
|
25
|
-
// fall through
|
|
44
|
+
// fall through
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
let ok = await composerEmpty(input, 5000);
|
|
29
|
-
|
|
30
48
|
if (!ok) {
|
|
31
|
-
const btn = page
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} catch (e) {
|
|
35
|
-
// no button
|
|
49
|
+
const btn = await findFirstVisible(page, provider.submit, 1500);
|
|
50
|
+
if (btn) {
|
|
51
|
+
try { await btn.click({ timeout: 1500 }); } catch (e) {}
|
|
36
52
|
}
|
|
37
53
|
ok = await composerEmpty(input, 3000);
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
if (!ok) {
|
|
41
57
|
throw new Error(
|
|
42
|
-
"Message stayed in composer
|
|
43
|
-
"Cancel/Send bar is showing, click Send manually
|
|
58
|
+
"Message stayed in composer.\n" +
|
|
59
|
+
"Check Chrome: if a Cancel/Send bar is showing, click Send manually, " +
|
|
60
|
+
"then resend."
|
|
44
61
|
);
|
|
45
62
|
}
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let appeared = false;
|
|
50
|
-
|
|
51
|
-
while (Date.now() < end) {
|
|
52
|
-
const c = await page.locator(provider.response).count();
|
|
53
|
-
if (c > before) {
|
|
54
|
-
appeared = true;
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
await sleep(300);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!appeared) throw new Error("No response appeared within the timeout.");
|
|
64
|
+
return waitForReply(page, provider, want, timeoutMs);
|
|
65
|
+
}
|
|
61
66
|
|
|
67
|
+
async function waitForReply(page, provider, want, timeoutMs) {
|
|
68
|
+
const end = Date.now() + timeoutMs;
|
|
62
69
|
let last = "";
|
|
63
70
|
let stab = Date.now();
|
|
71
|
+
let sawAnything = false;
|
|
72
|
+
const stream = makeStreamer();
|
|
73
|
+
|
|
74
|
+
process.stdout.write("\n" + theme.cyan("* ") + " ");
|
|
64
75
|
|
|
65
76
|
while (Date.now() < end) {
|
|
66
|
-
const node = page
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
tx = (await node.textContent()) || "";
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
const node = await findFirstVisible(page, provider.response, 1000);
|
|
78
|
+
if (node) {
|
|
79
|
+
let tx = "";
|
|
80
|
+
try { tx = (await node.textContent()) || ""; } catch (e) { tx = ""; }
|
|
81
|
+
const n = norm(tx);
|
|
82
|
+
|
|
83
|
+
if (!n || n === want) {
|
|
84
|
+
await sleep(POLL_MS);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (tx !== last) {
|
|
89
|
+
stream(tx);
|
|
90
|
+
last = tx;
|
|
91
|
+
stab = Date.now();
|
|
92
|
+
sawAnything = true;
|
|
93
|
+
} else if (sawAnything && Date.now() - stab > SETTLE_MS) {
|
|
94
|
+
process.stdout.write("\n\n");
|
|
95
|
+
return tx;
|
|
96
|
+
}
|
|
83
97
|
}
|
|
84
|
-
await sleep(
|
|
98
|
+
await sleep(POLL_MS);
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
process.stdout.write("\n\n");
|
|
102
|
+
if (last) return last;
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Timed out waiting for a reply.\n" +
|
|
105
|
+
"Check the Chrome window. If the model answered but Portal missed it, " +
|
|
106
|
+
"send the task again."
|
|
107
|
+
);
|
|
88
108
|
}
|
package/lib/sessions.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { mk
|
package/lib/system.mjs
CHANGED
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
import { CATALOG } from "./tools.mjs";
|
|
1
|
+
import { CATALOG } from "./tools/index.mjs";
|
|
2
2
|
|
|
3
|
-
export function buildSystem(ws) {
|
|
4
|
-
const shown = ws.split("\\
|
|
5
|
-
return [
|
|
6
|
-
"You are a coding agent working in a terminal.",
|
|
7
|
-
"Workspace: " + shown,
|
|
8
|
-
"Use forward slashes in every path, even on Windows.",
|
|
9
|
-
"",
|
|
10
|
-
"To call a tool, write on its own line:",
|
|
11
|
-
"portal-tool: {\"tool\":\"name\",\"args\":{\"key\":\"value\"}}",
|
|
12
|
-
"",
|
|
13
|
-
"The JSON must use double quotes around keys and values.",
|
|
14
|
-
"Plain text only. Do not wrap the call in a code block.",
|
|
15
|
-
"One call per action. Never repeat a call with the same arguments.",
|
|
16
|
-
"",
|
|
17
|
-
"If a tool returns an empty result, that is final. Report it.",
|
|
18
|
-
"If unsure what to do, reply with plain prose and ask.",
|
|
19
|
-
"When done, reply with plain prose and no tool calls.",
|
|
20
|
-
"",
|
|
21
|
-
"Available tools:",
|
|
22
|
-
CATALOG
|
|
23
|
-
].join("\n");
|
|
24
|
-
}
|
|
3
|
+
export function buildSystem(ws, instructions) {
|
|
4
|
+
const shown = ws.split("\\
|
package/lib/theme.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const E = String.fromCharCode(27);
|
|
2
|
+
|
|
3
|
+
const MODE = process.env.NO_COLOR ? "plain" : "color";
|
|
4
|
+
|
|
5
|
+
function c(code) {
|
|
6
|
+
if (MODE === "plain") return "";
|
|
7
|
+
return E + "[" + code + "m";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const R = c(0);
|
|
11
|
+
const B = c(1);
|
|
12
|
+
const D = c(90);
|
|
13
|
+
const RED = c(31);
|
|
14
|
+
const GRN = c(32);
|
|
15
|
+
const YEL = c(33);
|
|
16
|
+
const BLU = c(34);
|
|
17
|
+
const MAG = c(35);
|
|
18
|
+
const CYN = c(36);
|
|
19
|
+
const BGRN = c(92);
|
|
20
|
+
const BRED = c(91);
|
|
21
|
+
const BCYN = c(96);
|
|
22
|
+
|
|
23
|
+
function wrap(prefix, s) {
|
|
24
|
+
return prefix + String(s) + R;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const theme = {
|
|
28
|
+
reset: R,
|
|
29
|
+
bold: function (s) { return wrap(B, s); },
|
|
30
|
+
dim: function (s) { return wrap(D, s); },
|
|
31
|
+
red: function (s) { return wrap(RED, s); },
|
|
32
|
+
error: function (s) { return wrap(BRED, s); },
|
|
33
|
+
green: function (s) { return wrap(GRN, s); },
|
|
34
|
+
ok: function (s) { return wrap(BGRN, s); },
|
|
35
|
+
yellow: function (s) { return wrap(YEL, s); },
|
|
36
|
+
warn: function (s) { return wrap(YEL, s); },
|
|
37
|
+
blue: function (s) { return wrap(BLU, s); },
|
|
38
|
+
magenta: function (s) { return wrap(MAG, s); },
|
|
39
|
+
cyan: function (s) { return wrap(BCYN, s); },
|
|
40
|
+
accent: function (s) { return wrap(CYN, s); },
|
|
41
|
+
user: function (s) { return wrap(BLU, s); },
|
|
42
|
+
tool: function (s) { return wrap(MAG, s); },
|
|
43
|
+
italic: function (s) { return c(3) + String(s) + R; },
|
|
44
|
+
underline: function (s) { return c(4) + String(s) + R; }
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function colorEnabled() {
|
|
48
|
+
return MODE === "color";
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { relative, sep } from "
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/prom
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { readdir } from "node
|
package/lib/turn.mjs
CHANGED
|
@@ -1,57 +1,2 @@
|
|
|
1
1
|
import { send } from "./send.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { tools, getWs } from "./tools.mjs";
|
|
4
|
-
import { buildSystem } from "./system.mjs";
|
|
5
|
-
|
|
6
|
-
export async function runTurn(page, provider, prompt, opts) {
|
|
7
|
-
const sys = buildSystem(getWs());
|
|
8
|
-
const seen = new Map();
|
|
9
|
-
let next = sys + "\n\nTask: " + prompt;
|
|
10
|
-
|
|
11
|
-
for (let i = 0; i < opts.maxSteps; i++) {
|
|
12
|
-
console.log(" waiting for " + provider.label + "...");
|
|
13
|
-
const r = await send(page, provider, next, opts.timeout);
|
|
14
|
-
const calls = parseCalls(r);
|
|
15
|
-
|
|
16
|
-
if (!calls.length) {
|
|
17
|
-
const prose = stripCalls(r);
|
|
18
|
-
if (prose) console.log("\n" + prose + "\n");
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const results = [];
|
|
23
|
-
let stop = false;
|
|
24
|
-
|
|
25
|
-
for (const c of calls) {
|
|
26
|
-
const sig = c.tool + ":" + JSON.stringify(c.args || {});
|
|
27
|
-
const n = (seen.get(sig) || 0) + 1;
|
|
28
|
-
seen.set(sig, n);
|
|
29
|
-
|
|
30
|
-
if (n >= 3) {
|
|
31
|
-
console.log("loop detected: " + c.tool + " repeated. stopping.");
|
|
32
|
-
stop = true;
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
console.log("> " + c.tool + " " + JSON.stringify(c.args || {}).slice(0, 120));
|
|
37
|
-
|
|
38
|
-
let out;
|
|
39
|
-
try {
|
|
40
|
-
const fn = tools[c.tool];
|
|
41
|
-
out = fn ? await fn(c.args || {}) : "unknown tool: " + c.tool;
|
|
42
|
-
} catch (e) {
|
|
43
|
-
out = "error: " + (e.message || e);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.log(" " + String(out).slice(0, 240).split("\n").join("\n "));
|
|
47
|
-
results.push(c.tool + ":\n" + out);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (stop) return;
|
|
51
|
-
|
|
52
|
-
next = "Results:\n\n" + results.join("\n\n") +
|
|
53
|
-
"\n\nContinue. Do not repeat any call with the same arguments.";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
console.log("max steps reached for this turn.");
|
|
57
|
-
}
|
|
2
|
+
import { parse
|
package/lib/wait.mjs
CHANGED
|
@@ -6,27 +6,58 @@ export function norm(s) {
|
|
|
6
6
|
return String(s || "").replace(/\s+/g, " ").trim();
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export async function
|
|
9
|
+
export async function findFirstVisible(page, selectors, ms) {
|
|
10
10
|
const end = Date.now() + ms;
|
|
11
|
-
let hint = Date.now();
|
|
12
11
|
while (Date.now() < end) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
for (const sel of selectors) {
|
|
13
|
+
const loc = page.locator(sel).last();
|
|
14
|
+
try {
|
|
15
|
+
if (await loc.isVisible({ timeout: 200 })) return loc;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
// try next selector
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
await sleep(200);
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function waitForComposer(page, provider, ms) {
|
|
26
|
+
const end = Date.now() + ms;
|
|
27
|
+
let lastHint = Date.now();
|
|
28
|
+
while (Date.now() < end) {
|
|
29
|
+
for (const sel of provider.input) {
|
|
30
|
+
const loc = page.locator(sel).last();
|
|
31
|
+
let ok = false;
|
|
32
|
+
try {
|
|
33
|
+
ok = await loc.isVisible({ timeout: 200 });
|
|
34
|
+
} catch (e) {
|
|
35
|
+
ok = false;
|
|
36
|
+
}
|
|
37
|
+
if (ok) return loc;
|
|
19
38
|
}
|
|
20
|
-
if (
|
|
21
|
-
if (Date.now() - hint > 15000) {
|
|
39
|
+
if (Date.now() - lastHint > 15000) {
|
|
22
40
|
const left = Math.round((end - Date.now()) / 1000);
|
|
23
41
|
process.stdout.write(" waiting for login (" + left + "s left)\n");
|
|
24
|
-
|
|
42
|
+
lastHint = Date.now();
|
|
25
43
|
}
|
|
26
44
|
await sleep(1000);
|
|
27
45
|
}
|
|
28
46
|
throw new Error(
|
|
29
|
-
"Timed out waiting for the composer
|
|
30
|
-
"and try again,
|
|
47
|
+
"Timed out waiting for the composer.\n" +
|
|
48
|
+
"Log in to the provider in the Chrome window and try again, " +
|
|
49
|
+
"or raise --login-timeout."
|
|
31
50
|
);
|
|
32
51
|
}
|
|
52
|
+
|
|
53
|
+
export async function countMatching(page, selectors) {
|
|
54
|
+
for (const sel of selectors) {
|
|
55
|
+
try {
|
|
56
|
+
const n = await page.locator(sel).count();
|
|
57
|
+
if (n > 0) return n;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// try next
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|