portal-agent-cli 3.0.0 → 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 +18 -0
- package/README.md +13 -40
- package/lib/args.mjs +5 -29
- package/lib/browser.mjs +7 -24
- package/lib/config.mjs +20 -27
- package/lib/deepthink.mjs +45 -0
- package/lib/exec.mjs +16 -1
- package/lib/log.mjs +0 -4
- package/lib/parse.mjs +3 -21
- package/lib/paste.mjs +4 -12
- package/lib/providers.mjs +11 -11
- package/lib/repl.mjs +42 -91
- package/lib/safety.mjs +2 -6
- package/lib/send.mjs +33 -26
- package/lib/sessions.mjs +1 -12
- package/lib/system.mjs +17 -11
- package/lib/theme.mjs +1 -11
- package/lib/tools/delete.mjs +4 -9
- package/lib/tools/edit.mjs +8 -13
- package/lib/tools/find.mjs +3 -6
- package/lib/tools/git.mjs +1 -1
- package/lib/tools/grep.mjs +5 -8
- package/lib/tools/index.mjs +14 -14
- package/lib/tools/list.mjs +2 -5
- package/lib/tools/read.mjs +3 -5
- package/lib/tools/shell.mjs +21 -3
- package/lib/tools/walk.mjs +0 -2
- package/lib/tools/write.mjs +3 -14
- package/lib/turn.mjs +17 -36
- package/lib/wait.mjs +8 -15
- package/package.json +3 -5
- package/portal.mjs +3 -16
package/lib/tools/index.mjs
CHANGED
|
@@ -36,18 +36,18 @@ export const SAFE_TOOLS = new Set([
|
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
export const CATALOG = [
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
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
53
|
].join("\n");
|
package/lib/tools/list.mjs
CHANGED
|
@@ -7,8 +7,7 @@ export async function list_files(args) {
|
|
|
7
7
|
const entries = await readdir(abs, { withFileTypes: true });
|
|
8
8
|
|
|
9
9
|
if (!entries.length) {
|
|
10
|
-
return "EMPTY.
|
|
11
|
-
"Do not call list_files again on this path.";
|
|
10
|
+
return "EMPTY. Do not call list_files again on this path.";
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
const rows = [];
|
|
@@ -19,9 +18,7 @@ export async function list_files(args) {
|
|
|
19
18
|
try {
|
|
20
19
|
const st = await stat(full);
|
|
21
20
|
size = formatSize(st.size);
|
|
22
|
-
} catch (err) {
|
|
23
|
-
size = "";
|
|
24
|
-
}
|
|
21
|
+
} catch (err) {}
|
|
25
22
|
}
|
|
26
23
|
const tag = e.isDirectory() ? "[dir] " : " ";
|
|
27
24
|
rows.push(tag + e.name.padEnd(40) + " " + size);
|
package/lib/tools/read.mjs
CHANGED
|
@@ -8,16 +8,14 @@ export async function read_file(args) {
|
|
|
8
8
|
const text = await readFile(abs, "utf8");
|
|
9
9
|
|
|
10
10
|
if (text.length > MAX_BYTES) {
|
|
11
|
-
return text.slice(0, MAX_BYTES) + "\n[truncated
|
|
11
|
+
return text.slice(0, MAX_BYTES) + "\n[truncated]";
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const lines = text.split("\n");
|
|
15
15
|
const start = Math.max(1, Number(args.start_line) || 1);
|
|
16
16
|
const end = Math.min(lines.length, Number(args.end_line) || lines.length);
|
|
17
17
|
|
|
18
|
-
if (start > end)
|
|
19
|
-
return "invalid range: start_line " + start + " is after end_line " + end;
|
|
20
|
-
}
|
|
18
|
+
if (start > end) return "invalid range";
|
|
21
19
|
|
|
22
20
|
const width = String(end).length;
|
|
23
21
|
const out = [];
|
|
@@ -26,7 +24,7 @@ export async function read_file(args) {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
const header = start === 1 && end === lines.length
|
|
29
|
-
? lines.length + " lines
|
|
27
|
+
? lines.length + " lines"
|
|
30
28
|
: "lines " + start + "-" + end + " of " + lines.length;
|
|
31
29
|
|
|
32
30
|
return "[" + header + "]\n" + out.join("\n");
|
package/lib/tools/shell.mjs
CHANGED
|
@@ -4,9 +4,26 @@ import { getWorkspace } from "./path.mjs";
|
|
|
4
4
|
const MAX_OUTPUT = 65000;
|
|
5
5
|
const TIMEOUT_MS = 60000;
|
|
6
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
|
+
|
|
7
22
|
export function shell(args) {
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
23
|
+
const raw = String(args.command || "");
|
|
24
|
+
if (!raw) return Promise.resolve("shell requires a command");
|
|
25
|
+
|
|
26
|
+
const cmd = normalizeWinCommand(raw);
|
|
10
27
|
|
|
11
28
|
return new Promise(function (done) {
|
|
12
29
|
const ws = getWorkspace();
|
|
@@ -26,7 +43,7 @@ export function shell(args) {
|
|
|
26
43
|
|
|
27
44
|
const timer = setTimeout(function () {
|
|
28
45
|
child.kill();
|
|
29
|
-
done("timed out
|
|
46
|
+
done("timed out\n" + stderr);
|
|
30
47
|
}, TIMEOUT_MS);
|
|
31
48
|
|
|
32
49
|
child.on("error", function (err) {
|
|
@@ -38,6 +55,7 @@ export function shell(args) {
|
|
|
38
55
|
clearTimeout(timer);
|
|
39
56
|
const both = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join("\n");
|
|
40
57
|
let s = "exit " + (code === null ? "null" : code) + "\n" + (both || "(no output)");
|
|
58
|
+
if (cmd !== raw) s += "\n[normalized: " + cmd + "]";
|
|
41
59
|
if (s.length > MAX_OUTPUT) s = s.slice(0, MAX_OUTPUT) + "\n[truncated]";
|
|
42
60
|
done(s);
|
|
43
61
|
});
|
package/lib/tools/walk.mjs
CHANGED
|
@@ -13,8 +13,6 @@ export function setIgnore(names) {
|
|
|
13
13
|
ignore = new Set(names || []);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// Depth-first file walker. Yields full absolute paths. Skips ignored dirs
|
|
17
|
-
// and hidden files (except .env.example which is often relevant).
|
|
18
16
|
export async function* walk(dir) {
|
|
19
17
|
let items;
|
|
20
18
|
try {
|
package/lib/tools/write.mjs
CHANGED
|
@@ -2,40 +2,29 @@ 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
|
-
// Write a whole file. Creates parent directories. If the file already
|
|
6
|
-
// exists, saves a backup first.
|
|
7
5
|
export async function write_file(args) {
|
|
8
6
|
const abs = safePath(args.path);
|
|
9
7
|
const content = args.content || "";
|
|
10
8
|
await mkdir(dirname(abs), { recursive: true });
|
|
11
9
|
|
|
12
|
-
// Backup existing file
|
|
13
10
|
try {
|
|
14
11
|
const existing = await readFile(abs, "utf8");
|
|
15
12
|
if (existing !== content) {
|
|
16
|
-
|
|
17
|
-
await writeFile(backup, existing, "utf8");
|
|
13
|
+
await writeFile(abs + ".portal-bak", existing, "utf8");
|
|
18
14
|
}
|
|
19
|
-
} catch (e) {
|
|
20
|
-
// new file, no backup needed
|
|
21
|
-
}
|
|
15
|
+
} catch (e) {}
|
|
22
16
|
|
|
23
17
|
await writeFile(abs, content, "utf8");
|
|
24
18
|
const lines = content.length === 0 ? 0 : content.split("\n").length;
|
|
25
19
|
return "wrote " + content.length + " bytes (" + lines + " lines) to " + args.path;
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
// Append to end of a file, creating it if missing.
|
|
29
22
|
export async function append_file(args) {
|
|
30
23
|
const abs = safePath(args.path);
|
|
31
24
|
await mkdir(dirname(abs), { recursive: true });
|
|
32
25
|
|
|
33
26
|
let existing = "";
|
|
34
|
-
try {
|
|
35
|
-
existing = await readFile(abs, "utf8");
|
|
36
|
-
} catch (e) {
|
|
37
|
-
existing = "";
|
|
38
|
-
}
|
|
27
|
+
try { existing = await readFile(abs, "utf8"); } catch (e) { existing = ""; }
|
|
39
28
|
|
|
40
29
|
const content = args.content || "";
|
|
41
30
|
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
package/lib/turn.mjs
CHANGED
|
@@ -16,8 +16,7 @@ export async function runTurn(page, provider, prompt, config, session) {
|
|
|
16
16
|
|
|
17
17
|
const seen = new Map();
|
|
18
18
|
let next = sys + "\n\nTask: " + prompt;
|
|
19
|
-
|
|
20
|
-
const newTools = [];
|
|
19
|
+
const approvedAlways = new Set(session.approvedAlways || []);
|
|
21
20
|
|
|
22
21
|
for (let step = 0; step < config.maxSteps; step++) {
|
|
23
22
|
status("waiting for " + provider.label + "...");
|
|
@@ -44,75 +43,57 @@ export async function runTurn(page, provider, prompt, config, session) {
|
|
|
44
43
|
|
|
45
44
|
if (n >= REPEAT_STOP) {
|
|
46
45
|
process.stdout.write(theme.error("x loop detected: " + c.tool +
|
|
47
|
-
"
|
|
46
|
+
" repeated. stopping.") + "\n");
|
|
48
47
|
stopped = true;
|
|
49
48
|
break;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
if (n === REPEAT_WARN) {
|
|
53
|
-
process.stdout.write(theme.warn("! " + c.tool + " repeated
|
|
54
|
-
" times. warning the model.") + "\n");
|
|
52
|
+
process.stdout.write(theme.warn("! " + c.tool + " repeated.") + "\n");
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
logTool(c.tool, c.args);
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
const needsApproval = !config.yolo &&
|
|
57
|
+
const needs = !config.yolo &&
|
|
61
58
|
!SAFE_TOOLS.has(c.tool) &&
|
|
62
59
|
!approvedAlways.has(c.tool);
|
|
63
60
|
|
|
64
|
-
if (
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
61
|
+
if (needs) {
|
|
62
|
+
const d = await askApproval(c.tool, c.args);
|
|
63
|
+
if (d === "no") {
|
|
67
64
|
const msg = "user denied " + c.tool;
|
|
68
65
|
toolResult(msg, true);
|
|
69
66
|
results.push("Tool: " + c.tool + "\nStatus: denied\n" + msg);
|
|
70
67
|
continue;
|
|
71
68
|
}
|
|
72
|
-
if (
|
|
73
|
-
approvedAlways.add(c.tool);
|
|
74
|
-
newTools.push(c.tool);
|
|
75
|
-
}
|
|
69
|
+
if (d === "always") approvedAlways.add(c.tool);
|
|
76
70
|
}
|
|
77
71
|
|
|
78
72
|
let out;
|
|
79
|
-
let
|
|
73
|
+
let isErr = false;
|
|
80
74
|
try {
|
|
81
75
|
const fn = TOOLS[c.tool];
|
|
82
|
-
if (!fn) {
|
|
83
|
-
|
|
84
|
-
isError = true;
|
|
85
|
-
} else {
|
|
86
|
-
out = await fn(c.args || {});
|
|
87
|
-
if (typeof out === "string" && out.indexOf("failed") === 0) isError = true;
|
|
88
|
-
}
|
|
76
|
+
if (!fn) { out = "unknown tool: " + c.tool; isErr = true; }
|
|
77
|
+
else out = await fn(c.args || {});
|
|
89
78
|
} catch (e) {
|
|
90
79
|
out = "error: " + (e.message || e);
|
|
91
|
-
|
|
80
|
+
isErr = true;
|
|
92
81
|
}
|
|
93
82
|
|
|
94
|
-
toolResult(out,
|
|
83
|
+
toolResult(out, isErr);
|
|
95
84
|
results.push("Tool: " + c.tool + "\nResult:\n" + out);
|
|
96
85
|
}
|
|
97
86
|
|
|
98
87
|
if (stopped) {
|
|
99
88
|
session.turns.push({ role: "user", text: prompt });
|
|
100
|
-
session.turns.push({ role: "assistant", text: "(stopped: loop
|
|
89
|
+
session.turns.push({ role: "assistant", text: "(stopped: loop)" });
|
|
101
90
|
session.updatedAt = Date.now();
|
|
102
91
|
return;
|
|
103
92
|
}
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
"Do not repeat any call with the same arguments.",
|
|
108
|
-
"If the task is complete, reply with plain prose and no tool calls."
|
|
109
|
-
].join("\n");
|
|
110
|
-
|
|
111
|
-
next = "Tool results:\n\n" + results.join("\n\n") + "\n\n" + footer;
|
|
94
|
+
next = "Tool results:\n\n" + results.join("\n\n") +
|
|
95
|
+
"\n\nContinue. Do not repeat any call.";
|
|
112
96
|
}
|
|
113
97
|
|
|
114
|
-
process.stdout.write(theme.warn("max steps reached
|
|
115
|
-
session.turns.push({ role: "user", text: prompt });
|
|
116
|
-
session.turns.push({ role: "assistant", text: "(max steps reached)" });
|
|
117
|
-
session.updatedAt = Date.now();
|
|
98
|
+
process.stdout.write(theme.warn("max steps reached.") + "\n");
|
|
118
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,9 +29,7 @@
|
|
|
29
29
|
"ai",
|
|
30
30
|
"agent",
|
|
31
31
|
"deepseek",
|
|
32
|
-
"chatgpt"
|
|
33
|
-
"gemini",
|
|
34
|
-
"playwright"
|
|
32
|
+
"chatgpt"
|
|
35
33
|
],
|
|
36
34
|
"license": "MIT"
|
|
37
35
|
}
|
package/portal.mjs
CHANGED
|
@@ -1,10 +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, and dispatches to either the
|
|
6
|
-
// interactive REPL or the one-shot exec mode.
|
|
7
|
-
//
|
|
8
2
|
import { parseArgs } from "./lib/args.mjs";
|
|
9
3
|
import { loadConfig } from "./lib/config.mjs";
|
|
10
4
|
import { runRepl } from "./lib/repl.mjs";
|
|
@@ -14,23 +8,16 @@ import { theme } from "./lib/theme.mjs";
|
|
|
14
8
|
async function main() {
|
|
15
9
|
const opts = parseArgs();
|
|
16
10
|
const config = await loadConfig(opts);
|
|
17
|
-
|
|
18
11
|
if (config.verbose) {
|
|
19
12
|
process.stdout.write(theme.dim("config: " + JSON.stringify(config, null, 2)) + "\n");
|
|
20
13
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
await runExec(config);
|
|
24
|
-
} else {
|
|
25
|
-
await runRepl(config);
|
|
26
|
-
}
|
|
14
|
+
if (opts.exec) await runExec(config);
|
|
15
|
+
else await runRepl(config);
|
|
27
16
|
}
|
|
28
17
|
|
|
29
18
|
main().catch(function (e) {
|
|
30
19
|
const msg = e && e.message ? e.message : String(e);
|
|
31
20
|
process.stderr.write(theme.error("fatal: " + msg) + "\n");
|
|
32
|
-
if (process.env.PORTAL_DEBUG)
|
|
33
|
-
process.stderr.write(e.stack + "\n");
|
|
34
|
-
}
|
|
21
|
+
if (process.env.PORTAL_DEBUG) process.stderr.write(e.stack + "\n");
|
|
35
22
|
process.exit(1);
|
|
36
23
|
});
|