portal-agent-cli 3.0.3 → 3.0.5
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 +0 -15
- package/README.md +0 -27
- package/lib/args.mjs +3 -12
- package/lib/browser.mjs +5 -20
- package/lib/config.mjs +3 -17
- package/lib/deepthink.mjs +4 -25
- package/lib/exec.mjs +1 -46
- package/lib/instructions.mjs +2 -32
- package/lib/log.mjs +5 -24
- package/lib/parse.mjs +4 -12
- package/lib/paste.mjs +6 -29
- package/lib/providers.mjs +11 -22
- package/lib/repl.mjs +1 -236
- package/lib/safety.mjs +36 -37
- package/lib/send.mjs +15 -24
- package/lib/sessions.mjs +1 -64
- package/lib/system.mjs +1 -40
- package/lib/theme.mjs +1 -5
- package/lib/tools/delete.mjs +1 -24
- package/lib/tools/edit.mjs +1 -46
- package/lib/tools/find.mjs +1 -38
- package/lib/tools/git.mjs +1 -49
- package/lib/tools/grep.mjs +1 -41
- package/lib/tools/index.mjs +1 -51
- package/lib/tools/list.mjs +1 -34
- package/lib/tools/path.mjs +1 -18
- package/lib/tools/read.mjs +1 -31
- package/lib/tools/shell.mjs +1 -59
- package/lib/tools/walk.mjs +1 -27
- package/lib/tools/write.mjs +1 -34
- package/lib/turn.mjs +1 -98
- package/lib/wait.mjs +2 -7
- package/package.json +1 -13
- package/portal.mjs +0 -4
package/lib/tools/git.mjs
CHANGED
|
@@ -1,52 +1,4 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { getWorkspace } from "./path.mjs";
|
|
3
3
|
|
|
4
|
-
function run(args
|
|
5
|
-
return new Promise(function (done) {
|
|
6
|
-
const child = spawn("git", args, {
|
|
7
|
-
cwd: getWorkspace(),
|
|
8
|
-
shell: false,
|
|
9
|
-
windowsHide: true
|
|
10
|
-
});
|
|
11
|
-
let out = "";
|
|
12
|
-
let err = "";
|
|
13
|
-
if (child.stdout) child.stdout.on("data", function (d) { out += String(d); });
|
|
14
|
-
if (child.stderr) child.stderr.on("data", function (d) { err += String(d); });
|
|
15
|
-
const timer = setTimeout(function () {
|
|
16
|
-
child.kill();
|
|
17
|
-
done({ code: -1, out: out, err: "timed out" });
|
|
18
|
-
}, 15000);
|
|
19
|
-
child.on("error", function (e) {
|
|
20
|
-
clearTimeout(timer);
|
|
21
|
-
done({ code: -1, out: "", err: e.message });
|
|
22
|
-
});
|
|
23
|
-
child.on("close", function (code) {
|
|
24
|
-
clearTimeout(timer);
|
|
25
|
-
done({ code: code, out: out, err: err });
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function git_status() {
|
|
31
|
-
const r = await run(["status", "--short", "--branch"]);
|
|
32
|
-
if (r.code !== 0) return "not a git repository";
|
|
33
|
-
return r.out.trim() || "working tree clean";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function git_diff(args) {
|
|
37
|
-
const cmd = ["diff"];
|
|
38
|
-
if (args && args.staged) cmd.push("--cached");
|
|
39
|
-
if (args && args.path) cmd.push("--", args.path);
|
|
40
|
-
const r = await run(cmd);
|
|
41
|
-
if (r.code !== 0) return r.err || "git diff failed";
|
|
42
|
-
const out = r.out.trim();
|
|
43
|
-
if (!out) return "no changes";
|
|
44
|
-
return out.length > 30000 ? out.slice(0, 30000) + "\n[truncated]" : out;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function git_log(args) {
|
|
48
|
-
const n = (args && args.count) || 10;
|
|
49
|
-
const r = await run(["log", "--oneline", "-" + n]);
|
|
50
|
-
if (r.code !== 0) return "not a git repository";
|
|
51
|
-
return r.out.trim() || "no commits";
|
|
52
|
-
}
|
|
4
|
+
function run(args
|
package/lib/tools/grep.mjs
CHANGED
|
@@ -1,41 +1 @@
|
|
|
1
|
-
import { readFile, stat } from "node:fs/
|
|
2
|
-
import { relative } from "node:path";
|
|
3
|
-
import { walk } from "./walk.mjs";
|
|
4
|
-
import { safePath, toPosix, getWorkspace } from "./path.mjs";
|
|
5
|
-
|
|
6
|
-
export async function grep_search(args) {
|
|
7
|
-
const pattern = String(args.pattern || "");
|
|
8
|
-
if (!pattern) return "grep_search requires a pattern";
|
|
9
|
-
|
|
10
|
-
let re;
|
|
11
|
-
try {
|
|
12
|
-
re = new RegExp(pattern, args.case_sensitive ? "" : "i");
|
|
13
|
-
} catch (e) {
|
|
14
|
-
return "bad regex: " + e.message;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const ws = getWorkspace();
|
|
18
|
-
const startPath = args.path ? safePath(args.path) : ws;
|
|
19
|
-
const hits = [];
|
|
20
|
-
|
|
21
|
-
for await (const f of walk(startPath)) {
|
|
22
|
-
if (hits.length >= 80) break;
|
|
23
|
-
let info;
|
|
24
|
-
try { info = await stat(f); } catch (e) { continue; }
|
|
25
|
-
if (info.size > 512000) continue;
|
|
26
|
-
|
|
27
|
-
let text;
|
|
28
|
-
try { text = await readFile(f, "utf8"); } catch (e) { continue; }
|
|
29
|
-
const lines = text.split("\n");
|
|
30
|
-
|
|
31
|
-
for (let i = 0; i < lines.length && hits.length < 80; i++) {
|
|
32
|
-
if (re.test(lines[i])) {
|
|
33
|
-
const rel = toPosix(relative(ws, f));
|
|
34
|
-
hits.push(rel + ":" + (i + 1) + ":" + lines[i].trim().slice(0, 200));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!hits.length) return "no matches";
|
|
40
|
-
return hits.length + " matches:\n" + hits.join("\n");
|
|
41
|
-
}
|
|
1
|
+
import { readFile, stat } from "node:fs/p
|
package/lib/tools/index.mjs
CHANGED
|
@@ -1,53 +1,3 @@
|
|
|
1
1
|
import { read_file } from "./read.mjs";
|
|
2
2
|
import { write_file, append_file } from "./write.mjs";
|
|
3
|
-
import { edit_file, undo_file } from "./edit.
|
|
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");
|
|
3
|
+
import { edit_file, undo_file } from "./edit.m
|
package/lib/tools/list.mjs
CHANGED
|
@@ -1,35 +1,2 @@
|
|
|
1
1
|
import { readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { join } from
|
|
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
|
-
}
|
|
2
|
+
import { join } from
|
package/lib/tools/path.mjs
CHANGED
|
@@ -2,21 +2,4 @@ import { resolve, relative, sep } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
let ws = process.cwd();
|
|
4
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
|
-
}
|
|
5
|
+
export function setWorkspace(p) { ws = resolve(p
|
package/lib/tools/read.mjs
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { safePath } from "./path.mjs";
|
|
3
|
-
|
|
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
|
-
}
|
|
1
|
+
import {
|
package/lib/tools/shell.mjs
CHANGED
|
@@ -2,62 +2,4 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { getWorkspace } from "./path.mjs";
|
|
3
3
|
|
|
4
4
|
const MAX_OUTPUT = 65000;
|
|
5
|
-
const
|
|
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
|
-
}
|
|
5
|
+
const TIMEO
|
package/lib/tools/walk.mjs
CHANGED
|
@@ -1,30 +1,4 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
const DEFAULT_IGNORE = new
|
|
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
|
-
}
|
|
4
|
+
const DEFAULT_IGNORE = new
|
package/lib/tools/write.mjs
CHANGED
|
@@ -1,35 +1,2 @@
|
|
|
1
1
|
import { writeFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path
|
|
3
|
-
import { safePath } from "./path.mjs";
|
|
4
|
-
|
|
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
|
-
}
|
|
2
|
+
import { dirname } from "node:path
|
package/lib/turn.mjs
CHANGED
|
@@ -1,99 +1,2 @@
|
|
|
1
1
|
import { send } from "./send.mjs";
|
|
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
|
-
}
|
|
2
|
+
import { parseCalls, stripCalls } from "./parse.mjs
|
package/lib/wait.mjs
CHANGED
|
@@ -27,9 +27,7 @@ export async function waitForComposer(page, provider, ms) {
|
|
|
27
27
|
for (const sel of provider.input) {
|
|
28
28
|
const loc = page.locator(sel).last();
|
|
29
29
|
let ok = false;
|
|
30
|
-
try {
|
|
31
|
-
ok = await loc.isVisible({ timeout: 200 });
|
|
32
|
-
} catch (e) {}
|
|
30
|
+
try { ok = await loc.isVisible({ timeout: 200 }); } catch (e) {}
|
|
33
31
|
if (ok) return loc;
|
|
34
32
|
}
|
|
35
33
|
if (Date.now() - hint > 15000) {
|
|
@@ -39,10 +37,7 @@ export async function waitForComposer(page, provider, ms) {
|
|
|
39
37
|
}
|
|
40
38
|
await sleep(1000);
|
|
41
39
|
}
|
|
42
|
-
throw new Error(
|
|
43
|
-
"Timed out waiting for the composer. Log in to the provider " +
|
|
44
|
-
"and try again, or raise --login-timeout."
|
|
45
|
-
);
|
|
40
|
+
throw new Error("Timed out waiting for composer.");
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
export async function countMatching(page, selectors) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-agent-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "Drive web AI products from the terminal with local tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,20 +16,8 @@
|
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=20"
|
|
18
18
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"start": "node portal.mjs"
|
|
21
|
-
},
|
|
22
19
|
"dependencies": {
|
|
23
20
|
"playwright": "^1.48.0"
|
|
24
21
|
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"browser",
|
|
27
|
-
"automation",
|
|
28
|
-
"cli",
|
|
29
|
-
"ai",
|
|
30
|
-
"agent",
|
|
31
|
-
"deepseek",
|
|
32
|
-
"chatgpt"
|
|
33
|
-
],
|
|
34
22
|
"license": "MIT"
|
|
35
23
|
}
|
package/portal.mjs
CHANGED
|
@@ -8,9 +8,6 @@ import { theme } from "./lib/theme.mjs";
|
|
|
8
8
|
async function main() {
|
|
9
9
|
const opts = parseArgs();
|
|
10
10
|
const config = await loadConfig(opts);
|
|
11
|
-
if (config.verbose) {
|
|
12
|
-
process.stdout.write(theme.dim("config: " + JSON.stringify(config, null, 2)) + "\n");
|
|
13
|
-
}
|
|
14
11
|
if (opts.exec) await runExec(config);
|
|
15
12
|
else await runRepl(config);
|
|
16
13
|
}
|
|
@@ -18,6 +15,5 @@ async function main() {
|
|
|
18
15
|
main().catch(function (e) {
|
|
19
16
|
const msg = e && e.message ? e.message : String(e);
|
|
20
17
|
process.stderr.write(theme.error("fatal: " + msg) + "\n");
|
|
21
|
-
if (process.env.PORTAL_DEBUG) process.stderr.write(e.stack + "\n");
|
|
22
18
|
process.exit(1);
|
|
23
19
|
});
|