portal-agent-cli 3.0.1 → 3.0.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.
- package/LICENSE +17 -1
- package/README.md +13 -48
- package/lib/args.mjs +5 -31
- package/lib/browser.mjs +6 -21
- package/lib/config.mjs +20 -26
- package/lib/deepthink.mjs +11 -21
- package/lib/exec.mjs +45 -1
- package/lib/instructions.mjs +44 -1
- package/lib/log.mjs +0 -4
- package/lib/parse.mjs +42 -11
- package/lib/paste.mjs +4 -10
- package/lib/providers.mjs +7 -7
- package/lib/repl.mjs +234 -1
- package/lib/safety.mjs +60 -1
- package/lib/send.mjs +5 -14
- package/lib/sessions.mjs +64 -1
- package/lib/system.mjs +41 -1
- package/lib/theme.mjs +1 -4
- package/lib/tools/delete.mjs +24 -1
- package/lib/tools/edit.mjs +46 -1
- package/lib/tools/find.mjs +39 -1
- package/lib/tools/git.mjs +48 -1
- package/lib/tools/grep.mjs +40 -1
- package/lib/tools/index.mjs +52 -1
- package/lib/tools/list.mjs +35 -1
- package/lib/tools/path.mjs +22 -1
- package/lib/tools/read.mjs +28 -1
- package/lib/tools/shell.mjs +59 -1
- package/lib/tools/walk.mjs +30 -1
- package/lib/tools/write.mjs +31 -1
- package/lib/turn.mjs +98 -1
- package/lib/wait.mjs +8 -15
- package/package.json +3 -6
- package/portal.mjs +3 -15
package/lib/tools/index.mjs
CHANGED
|
@@ -1,2 +1,53 @@
|
|
|
1
1
|
import { read_file } from "./read.mjs";
|
|
2
|
-
import { write_file, append_file }
|
|
2
|
+
import { write_file, append_file } from "./write.mjs";
|
|
3
|
+
import { edit_file, undo_file } from "./edit.mjs";
|
|
4
|
+
import { list_files } from "./list.mjs";
|
|
5
|
+
import { find_files } from "./find.mjs";
|
|
6
|
+
import { grep_search } from "./grep.mjs";
|
|
7
|
+
import { delete_file, move_file } from "./delete.mjs";
|
|
8
|
+
import { shell } from "./shell.mjs";
|
|
9
|
+
import { git_status, git_diff, git_log } from "./git.mjs";
|
|
10
|
+
|
|
11
|
+
export const TOOLS = {
|
|
12
|
+
read_file: read_file,
|
|
13
|
+
write_file: write_file,
|
|
14
|
+
edit_file: edit_file,
|
|
15
|
+
append_file: append_file,
|
|
16
|
+
undo_file: undo_file,
|
|
17
|
+
list_files: list_files,
|
|
18
|
+
find_files: find_files,
|
|
19
|
+
grep_search: grep_search,
|
|
20
|
+
delete_file: delete_file,
|
|
21
|
+
move_file: move_file,
|
|
22
|
+
shell: shell,
|
|
23
|
+
git_status: git_status,
|
|
24
|
+
git_diff: git_diff,
|
|
25
|
+
git_log: git_log
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const SAFE_TOOLS = new Set([
|
|
29
|
+
"read_file",
|
|
30
|
+
"list_files",
|
|
31
|
+
"find_files",
|
|
32
|
+
"grep_search",
|
|
33
|
+
"git_status",
|
|
34
|
+
"git_diff",
|
|
35
|
+
"git_log"
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export const CATALOG = [
|
|
39
|
+
"read_file(path, start_line?, end_line?)",
|
|
40
|
+
"write_file(path, content)",
|
|
41
|
+
"edit_file(path, old_string, new_string, replace_all?)",
|
|
42
|
+
"append_file(path, content)",
|
|
43
|
+
"undo_file(path)",
|
|
44
|
+
"list_files(path?)",
|
|
45
|
+
"find_files(pattern)",
|
|
46
|
+
"grep_search(pattern, path?, case_sensitive?)",
|
|
47
|
+
"delete_file(path)",
|
|
48
|
+
"move_file(from, to)",
|
|
49
|
+
"shell(command)",
|
|
50
|
+
"git_status()",
|
|
51
|
+
"git_diff(path?, staged?)",
|
|
52
|
+
"git_log(count?)"
|
|
53
|
+
].join("\n");
|
package/lib/tools/list.mjs
CHANGED
|
@@ -1 +1,35 @@
|
|
|
1
|
-
import { readdir, stat } from "node:fs/
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { safePath } from "./path.mjs";
|
|
4
|
+
|
|
5
|
+
export async function list_files(args) {
|
|
6
|
+
const abs = safePath(args.path || ".");
|
|
7
|
+
const entries = await readdir(abs, { withFileTypes: true });
|
|
8
|
+
|
|
9
|
+
if (!entries.length) {
|
|
10
|
+
return "EMPTY. Do not call list_files again on this path.";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rows = [];
|
|
14
|
+
for (const e of entries) {
|
|
15
|
+
const full = join(abs, e.name);
|
|
16
|
+
let size = "";
|
|
17
|
+
if (e.isFile()) {
|
|
18
|
+
try {
|
|
19
|
+
const st = await stat(full);
|
|
20
|
+
size = formatSize(st.size);
|
|
21
|
+
} catch (err) {}
|
|
22
|
+
}
|
|
23
|
+
const tag = e.isDirectory() ? "[dir] " : " ";
|
|
24
|
+
rows.push(tag + e.name.padEnd(40) + " " + size);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
rows.sort();
|
|
28
|
+
return rows.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatSize(bytes) {
|
|
32
|
+
if (bytes < 1024) return bytes + " B";
|
|
33
|
+
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + " KB";
|
|
34
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
35
|
+
}
|
package/lib/tools/path.mjs
CHANGED
|
@@ -1 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { resolve, relative, sep } from "node:path";
|
|
2
|
+
|
|
3
|
+
let ws = process.cwd();
|
|
4
|
+
|
|
5
|
+
export function setWorkspace(p) { ws = resolve(p); }
|
|
6
|
+
export function getWorkspace() { return ws; }
|
|
7
|
+
|
|
8
|
+
export function safePath(p) {
|
|
9
|
+
if (!p || typeof p !== "string") {
|
|
10
|
+
throw new Error("path must be a non-empty string");
|
|
11
|
+
}
|
|
12
|
+
const abs = resolve(ws, p);
|
|
13
|
+
const rel = relative(ws, abs);
|
|
14
|
+
if (rel === ".." || rel.slice(0, 3) === "..\\" || rel.slice(0, 3) === "../") {
|
|
15
|
+
throw new Error("path escapes workspace: " + p);
|
|
16
|
+
}
|
|
17
|
+
return abs;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function toPosix(p) {
|
|
21
|
+
return p.split(sep).join("/");
|
|
22
|
+
}
|
package/lib/tools/read.mjs
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { safePath } from "./path.mjs";
|
|
3
3
|
|
|
4
|
-
const MAX_BYTES =
|
|
4
|
+
const MAX_BYTES = 500000;
|
|
5
|
+
|
|
6
|
+
export async function read_file(args) {
|
|
7
|
+
const abs = safePath(args.path);
|
|
8
|
+
const text = await readFile(abs, "utf8");
|
|
9
|
+
|
|
10
|
+
if (text.length > MAX_BYTES) {
|
|
11
|
+
return text.slice(0, MAX_BYTES) + "\n[truncated]";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lines = text.split("\n");
|
|
15
|
+
const start = Math.max(1, Number(args.start_line) || 1);
|
|
16
|
+
const end = Math.min(lines.length, Number(args.end_line) || lines.length);
|
|
17
|
+
|
|
18
|
+
if (start > end) return "invalid range";
|
|
19
|
+
|
|
20
|
+
const width = String(end).length;
|
|
21
|
+
const out = [];
|
|
22
|
+
for (let i = start - 1; i < end; i++) {
|
|
23
|
+
out.push(String(i + 1).padStart(width) + "| " + lines[i]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const header = start === 1 && end === lines.length
|
|
27
|
+
? lines.length + " lines"
|
|
28
|
+
: "lines " + start + "-" + end + " of " + lines.length;
|
|
29
|
+
|
|
30
|
+
return "[" + header + "]\n" + out.join("\n");
|
|
31
|
+
}
|
package/lib/tools/shell.mjs
CHANGED
|
@@ -2,4 +2,62 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { getWorkspace } from "./path.mjs";
|
|
3
3
|
|
|
4
4
|
const MAX_OUTPUT = 65000;
|
|
5
|
-
const TIMEOUT_MS = 60000
|
|
5
|
+
const TIMEOUT_MS = 60000;
|
|
6
|
+
|
|
7
|
+
function normalizeWinCommand(cmd) {
|
|
8
|
+
if (process.platform !== "win32") return cmd;
|
|
9
|
+
return cmd.replace(/(\S+)/g, function (token) {
|
|
10
|
+
if (token.indexOf("://") >= 0) return token;
|
|
11
|
+
if (/^\/[a-zA-Z](\s|$)/.test(token)) return token;
|
|
12
|
+
if (/^[A-Za-z]:\//.test(token) || (token.indexOf("/") > 0 && token.indexOf("://") < 0)) {
|
|
13
|
+
const leading = token.startsWith('"') ? '"' : "";
|
|
14
|
+
const trailing = token.endsWith('"') ? '"' : "";
|
|
15
|
+
const body = token.slice(leading.length, token.length - trailing.length);
|
|
16
|
+
return leading + body.split("/").join("\\") + trailing;
|
|
17
|
+
}
|
|
18
|
+
return token;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shell(args) {
|
|
23
|
+
const raw = String(args.command || "");
|
|
24
|
+
if (!raw) return Promise.resolve("shell requires a command");
|
|
25
|
+
|
|
26
|
+
const cmd = normalizeWinCommand(raw);
|
|
27
|
+
|
|
28
|
+
return new Promise(function (done) {
|
|
29
|
+
const ws = getWorkspace();
|
|
30
|
+
const isWin = process.platform === "win32";
|
|
31
|
+
|
|
32
|
+
const child = spawn(cmd, {
|
|
33
|
+
cwd: ws,
|
|
34
|
+
shell: isWin ? true : "/bin/sh",
|
|
35
|
+
windowsHide: true
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let stderr = "";
|
|
40
|
+
|
|
41
|
+
if (child.stdout) child.stdout.on("data", function (d) { stdout += String(d); });
|
|
42
|
+
if (child.stderr) child.stderr.on("data", function (d) { stderr += String(d); });
|
|
43
|
+
|
|
44
|
+
const timer = setTimeout(function () {
|
|
45
|
+
child.kill();
|
|
46
|
+
done("timed out\n" + stderr);
|
|
47
|
+
}, TIMEOUT_MS);
|
|
48
|
+
|
|
49
|
+
child.on("error", function (err) {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
done("spawn error: " + err.message);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
child.on("close", function (code) {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
const both = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join("\n");
|
|
57
|
+
let s = "exit " + (code === null ? "null" : code) + "\n" + (both || "(no output)");
|
|
58
|
+
if (cmd !== raw) s += "\n[normalized: " + cmd + "]";
|
|
59
|
+
if (s.length > MAX_OUTPUT) s = s.slice(0, MAX_OUTPUT) + "\n[truncated]";
|
|
60
|
+
done(s);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
package/lib/tools/walk.mjs
CHANGED
|
@@ -1 +1,30 @@
|
|
|
1
|
-
import { readdir } from "node
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_IGNORE = new Set([
|
|
5
|
+
"node_modules", ".git", ".hg", ".svn", "dist", "build", "out",
|
|
6
|
+
".next", ".nuxt", ".turbo", ".cache", "target", "__pycache__",
|
|
7
|
+
".venv", "venv", "env", "coverage", ".idea", ".vscode", ".DS_Store"
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
let ignore = DEFAULT_IGNORE;
|
|
11
|
+
|
|
12
|
+
export function setIgnore(names) {
|
|
13
|
+
ignore = new Set(names || []);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function* walk(dir) {
|
|
17
|
+
let items;
|
|
18
|
+
try {
|
|
19
|
+
items = await readdir(dir, { withFileTypes: true });
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const it of items) {
|
|
24
|
+
if (ignore.has(it.name)) continue;
|
|
25
|
+
if (it.name.startsWith(".") && it.name !== ".env.example") continue;
|
|
26
|
+
const full = join(dir, it.name);
|
|
27
|
+
if (it.isDirectory()) yield* walk(full);
|
|
28
|
+
else if (it.isFile()) yield full;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/tools/write.mjs
CHANGED
|
@@ -2,4 +2,34 @@ import { writeFile, mkdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { safePath } from "./path.mjs";
|
|
4
4
|
|
|
5
|
-
export async function write_file
|
|
5
|
+
export async function write_file(args) {
|
|
6
|
+
const abs = safePath(args.path);
|
|
7
|
+
const content = args.content || "";
|
|
8
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const existing = await readFile(abs, "utf8");
|
|
12
|
+
if (existing !== content) {
|
|
13
|
+
await writeFile(abs + ".portal-bak", existing, "utf8");
|
|
14
|
+
}
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
|
|
17
|
+
await writeFile(abs, content, "utf8");
|
|
18
|
+
const lines = content.length === 0 ? 0 : content.split("\n").length;
|
|
19
|
+
return "wrote " + content.length + " bytes (" + lines + " lines) to " + args.path;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function append_file(args) {
|
|
23
|
+
const abs = safePath(args.path);
|
|
24
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
25
|
+
|
|
26
|
+
let existing = "";
|
|
27
|
+
try { existing = await readFile(abs, "utf8"); } catch (e) { existing = ""; }
|
|
28
|
+
|
|
29
|
+
const content = args.content || "";
|
|
30
|
+
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
31
|
+
const add = content.endsWith("\n") ? content : content + "\n";
|
|
32
|
+
|
|
33
|
+
await writeFile(abs, existing + sep + add, "utf8");
|
|
34
|
+
return "appended " + content.length + " bytes to " + args.path;
|
|
35
|
+
}
|
package/lib/turn.mjs
CHANGED
|
@@ -1,2 +1,99 @@
|
|
|
1
1
|
import { send } from "./send.mjs";
|
|
2
|
-
import { parse
|
|
2
|
+
import { parseCalls, stripCalls } from "./parse.mjs";
|
|
3
|
+
import { TOOLS, SAFE_TOOLS } from "./tools/index.mjs";
|
|
4
|
+
import { getWorkspace } from "./tools/path.mjs";
|
|
5
|
+
import { buildSystem } from "./system.mjs";
|
|
6
|
+
import { askApproval } from "./safety.mjs";
|
|
7
|
+
import { reply, tool as logTool, toolResult, status } from "./log.mjs";
|
|
8
|
+
import { theme } from "./theme.mjs";
|
|
9
|
+
|
|
10
|
+
const REPEAT_WARN = 2;
|
|
11
|
+
const REPEAT_STOP = 3;
|
|
12
|
+
|
|
13
|
+
export async function runTurn(page, provider, prompt, config, session) {
|
|
14
|
+
const instructions = config.instructionsText || null;
|
|
15
|
+
const sys = buildSystem(getWorkspace(), instructions);
|
|
16
|
+
|
|
17
|
+
const seen = new Map();
|
|
18
|
+
let next = sys + "\n\nTask: " + prompt;
|
|
19
|
+
const approvedAlways = new Set(session.approvedAlways || []);
|
|
20
|
+
|
|
21
|
+
for (let step = 0; step < config.maxSteps; step++) {
|
|
22
|
+
status("waiting for " + provider.label + "...");
|
|
23
|
+
const r = await send(page, provider, next, config.timeout);
|
|
24
|
+
const calls = parseCalls(r);
|
|
25
|
+
|
|
26
|
+
if (!calls.length) {
|
|
27
|
+
const prose = stripCalls(r);
|
|
28
|
+
reply(prose);
|
|
29
|
+
session.turns.push({ role: "user", text: prompt });
|
|
30
|
+
session.turns.push({ role: "assistant", text: prose });
|
|
31
|
+
session.approvedAlways = Array.from(approvedAlways);
|
|
32
|
+
session.updatedAt = Date.now();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const results = [];
|
|
37
|
+
let stopped = false;
|
|
38
|
+
|
|
39
|
+
for (const c of calls) {
|
|
40
|
+
const sig = c.tool + ":" + JSON.stringify(c.args || {});
|
|
41
|
+
const n = (seen.get(sig) || 0) + 1;
|
|
42
|
+
seen.set(sig, n);
|
|
43
|
+
|
|
44
|
+
if (n >= REPEAT_STOP) {
|
|
45
|
+
process.stdout.write(theme.error("x loop detected: " + c.tool +
|
|
46
|
+
" repeated. stopping.") + "\n");
|
|
47
|
+
stopped = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (n === REPEAT_WARN) {
|
|
52
|
+
process.stdout.write(theme.warn("! " + c.tool + " repeated.") + "\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logTool(c.tool, c.args);
|
|
56
|
+
|
|
57
|
+
const needs = !config.yolo &&
|
|
58
|
+
!SAFE_TOOLS.has(c.tool) &&
|
|
59
|
+
!approvedAlways.has(c.tool);
|
|
60
|
+
|
|
61
|
+
if (needs) {
|
|
62
|
+
const d = await askApproval(c.tool, c.args);
|
|
63
|
+
if (d === "no") {
|
|
64
|
+
const msg = "user denied " + c.tool;
|
|
65
|
+
toolResult(msg, true);
|
|
66
|
+
results.push("Tool: " + c.tool + "\nStatus: denied\n" + msg);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (d === "always") approvedAlways.add(c.tool);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let out;
|
|
73
|
+
let isErr = false;
|
|
74
|
+
try {
|
|
75
|
+
const fn = TOOLS[c.tool];
|
|
76
|
+
if (!fn) { out = "unknown tool: " + c.tool; isErr = true; }
|
|
77
|
+
else out = await fn(c.args || {});
|
|
78
|
+
} catch (e) {
|
|
79
|
+
out = "error: " + (e.message || e);
|
|
80
|
+
isErr = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toolResult(out, isErr);
|
|
84
|
+
results.push("Tool: " + c.tool + "\nResult:\n" + out);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (stopped) {
|
|
88
|
+
session.turns.push({ role: "user", text: prompt });
|
|
89
|
+
session.turns.push({ role: "assistant", text: "(stopped: loop)" });
|
|
90
|
+
session.updatedAt = Date.now();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
next = "Tool results:\n\n" + results.join("\n\n") +
|
|
95
|
+
"\n\nContinue. Do not repeat any call.";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
process.stdout.write(theme.warn("max steps reached.") + "\n");
|
|
99
|
+
}
|
package/lib/wait.mjs
CHANGED
|
@@ -13,9 +13,7 @@ export async function findFirstVisible(page, selectors, ms) {
|
|
|
13
13
|
const loc = page.locator(sel).last();
|
|
14
14
|
try {
|
|
15
15
|
if (await loc.isVisible({ timeout: 200 })) return loc;
|
|
16
|
-
} catch (e) {
|
|
17
|
-
// try next selector
|
|
18
|
-
}
|
|
16
|
+
} catch (e) {}
|
|
19
17
|
}
|
|
20
18
|
await sleep(200);
|
|
21
19
|
}
|
|
@@ -24,29 +22,26 @@ export async function findFirstVisible(page, selectors, ms) {
|
|
|
24
22
|
|
|
25
23
|
export async function waitForComposer(page, provider, ms) {
|
|
26
24
|
const end = Date.now() + ms;
|
|
27
|
-
let
|
|
25
|
+
let hint = Date.now();
|
|
28
26
|
while (Date.now() < end) {
|
|
29
27
|
for (const sel of provider.input) {
|
|
30
28
|
const loc = page.locator(sel).last();
|
|
31
29
|
let ok = false;
|
|
32
30
|
try {
|
|
33
31
|
ok = await loc.isVisible({ timeout: 200 });
|
|
34
|
-
} catch (e) {
|
|
35
|
-
ok = false;
|
|
36
|
-
}
|
|
32
|
+
} catch (e) {}
|
|
37
33
|
if (ok) return loc;
|
|
38
34
|
}
|
|
39
|
-
if (Date.now() -
|
|
35
|
+
if (Date.now() - hint > 15000) {
|
|
40
36
|
const left = Math.round((end - Date.now()) / 1000);
|
|
41
37
|
process.stdout.write(" waiting for login (" + left + "s left)\n");
|
|
42
|
-
|
|
38
|
+
hint = Date.now();
|
|
43
39
|
}
|
|
44
40
|
await sleep(1000);
|
|
45
41
|
}
|
|
46
42
|
throw new Error(
|
|
47
|
-
"Timed out waiting for the composer
|
|
48
|
-
"
|
|
49
|
-
"or raise --login-timeout."
|
|
43
|
+
"Timed out waiting for the composer. Log in to the provider " +
|
|
44
|
+
"and try again, or raise --login-timeout."
|
|
50
45
|
);
|
|
51
46
|
}
|
|
52
47
|
|
|
@@ -55,9 +50,7 @@ export async function countMatching(page, selectors) {
|
|
|
55
50
|
try {
|
|
56
51
|
const n = await page.locator(sel).count();
|
|
57
52
|
if (n > 0) return n;
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// try next
|
|
60
|
-
}
|
|
53
|
+
} catch (e) {}
|
|
61
54
|
}
|
|
62
55
|
return 0;
|
|
63
56
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-agent-cli",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Drive web AI products from the terminal with local tools.
|
|
3
|
+
"version": "3.0.2",
|
|
4
|
+
"description": "Drive web AI products from the terminal with local tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"portal-agent": "portal.mjs"
|
|
@@ -29,10 +29,7 @@
|
|
|
29
29
|
"ai",
|
|
30
30
|
"agent",
|
|
31
31
|
"deepseek",
|
|
32
|
-
"chatgpt"
|
|
33
|
-
"gemini",
|
|
34
|
-
"grok",
|
|
35
|
-
"playwright"
|
|
32
|
+
"chatgpt"
|
|
36
33
|
],
|
|
37
34
|
"license": "MIT"
|
|
38
35
|
}
|
package/portal.mjs
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// portal-agent -- drive web AI products from the terminal with local tools.
|
|
4
|
-
//
|
|
5
|
-
// Entry point. Loads config, parses arguments, dispatches to REPL or exec.
|
|
6
|
-
//
|
|
7
2
|
import { parseArgs } from "./lib/args.mjs";
|
|
8
3
|
import { loadConfig } from "./lib/config.mjs";
|
|
9
4
|
import { runRepl } from "./lib/repl.mjs";
|
|
@@ -13,23 +8,16 @@ import { theme } from "./lib/theme.mjs";
|
|
|
13
8
|
async function main() {
|
|
14
9
|
const opts = parseArgs();
|
|
15
10
|
const config = await loadConfig(opts);
|
|
16
|
-
|
|
17
11
|
if (config.verbose) {
|
|
18
12
|
process.stdout.write(theme.dim("config: " + JSON.stringify(config, null, 2)) + "\n");
|
|
19
13
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await runExec(config);
|
|
23
|
-
} else {
|
|
24
|
-
await runRepl(config);
|
|
25
|
-
}
|
|
14
|
+
if (opts.exec) await runExec(config);
|
|
15
|
+
else await runRepl(config);
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
main().catch(function (e) {
|
|
29
19
|
const msg = e && e.message ? e.message : String(e);
|
|
30
20
|
process.stderr.write(theme.error("fatal: " + msg) + "\n");
|
|
31
|
-
if (process.env.PORTAL_DEBUG)
|
|
32
|
-
process.stderr.write(e.stack + "\n");
|
|
33
|
-
}
|
|
21
|
+
if (process.env.PORTAL_DEBUG) process.stderr.write(e.stack + "\n");
|
|
34
22
|
process.exit(1);
|
|
35
23
|
});
|