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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-agent-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Drive web AI products from the terminal with local tools.
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Drive web AI products from the terminal with local tools. Multi-provider, approval gate, session persistence, undo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"portal-agent": "portal.mjs"
|
|
@@ -17,15 +17,11 @@
|
|
|
17
17
|
"node": ">=20"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"start": "node portal.mjs"
|
|
21
|
-
"build": "pkg portal.mjs --targets node18-win-x64 --output portal.exe"
|
|
20
|
+
"start": "node portal.mjs"
|
|
22
21
|
},
|
|
23
22
|
"dependencies": {
|
|
24
23
|
"playwright": "^1.48.0"
|
|
25
24
|
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"pkg": "^5.8.1"
|
|
28
|
-
},
|
|
29
25
|
"keywords": [
|
|
30
26
|
"browser",
|
|
31
27
|
"automation",
|
|
@@ -34,8 +30,9 @@
|
|
|
34
30
|
"agent",
|
|
35
31
|
"deepseek",
|
|
36
32
|
"chatgpt",
|
|
33
|
+
"gemini",
|
|
34
|
+
"grok",
|
|
37
35
|
"playwright"
|
|
38
36
|
],
|
|
39
|
-
"author": "kitadestin",
|
|
40
37
|
"license": "MIT"
|
|
41
|
-
}
|
|
38
|
+
}
|
package/portal.mjs
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
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
|
+
//
|
|
2
7
|
import { parseArgs } from "./lib/args.mjs";
|
|
8
|
+
import { loadConfig } from "./lib/config.mjs";
|
|
3
9
|
import { runRepl } from "./lib/repl.mjs";
|
|
4
10
|
import { runExec } from "./lib/exec.mjs";
|
|
11
|
+
import { theme } from "./lib/theme.mjs";
|
|
5
12
|
|
|
6
|
-
|
|
13
|
+
async function main() {
|
|
14
|
+
const opts = parseArgs();
|
|
15
|
+
const config = await loadConfig(opts);
|
|
7
16
|
|
|
8
|
-
|
|
17
|
+
if (config.verbose) {
|
|
18
|
+
process.stdout.write(theme.dim("config: " + JSON.stringify(config, null, 2)) + "\n");
|
|
19
|
+
}
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
if (opts.exec) {
|
|
22
|
+
await runExec(config);
|
|
23
|
+
} else {
|
|
24
|
+
await runRepl(config);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main().catch(function (e) {
|
|
29
|
+
const msg = e && e.message ? e.message : String(e);
|
|
30
|
+
process.stderr.write(theme.error("fatal: " + msg) + "\n");
|
|
31
|
+
if (process.env.PORTAL_DEBUG) {
|
|
32
|
+
process.stderr.write(e.stack + "\n");
|
|
33
|
+
}
|
|
12
34
|
process.exit(1);
|
|
13
35
|
});
|
package/lib/tools.mjs
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve, relative, sep, join } from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
|
|
5
|
-
let ws = process.cwd();
|
|
6
|
-
|
|
7
|
-
export function setWs(p) {
|
|
8
|
-
ws = resolve(p);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getWs() {
|
|
12
|
-
return ws;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const IGNORE = new Set([
|
|
16
|
-
"node_modules", ".git", ".hg", ".svn", "dist", "build", "out",
|
|
17
|
-
".next", ".nuxt", ".turbo", ".cache", "target", "__pycache__",
|
|
18
|
-
".venv", "venv", "env", "coverage", ".idea", ".vscode"
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
function safe(p) {
|
|
22
|
-
const a = resolve(ws, p);
|
|
23
|
-
const r = relative(ws, a);
|
|
24
|
-
if (r === ".." || r.slice(0, 3) === "..\\" || r.slice(0, 3) === "../") {
|
|
25
|
-
throw new Error("path escapes workspace: " + p);
|
|
26
|
-
}
|
|
27
|
-
return a;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function* walk(dir) {
|
|
31
|
-
let items;
|
|
32
|
-
try {
|
|
33
|
-
items = await readdir(dir, { withFileTypes: true });
|
|
34
|
-
} catch (e) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
for (const it of items) {
|
|
38
|
-
if (IGNORE.has(it.name)) continue;
|
|
39
|
-
if (it.name.startsWith(".") && it.name !== ".env.example") continue;
|
|
40
|
-
const full = join(dir, it.name);
|
|
41
|
-
if (it.isDirectory()) yield* walk(full);
|
|
42
|
-
else if (it.isFile()) yield full;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const tools = {
|
|
47
|
-
read_file: async function (a) {
|
|
48
|
-
const t = await readFile(safe(a.path), "utf8");
|
|
49
|
-
return t.length > 200000 ? t.slice(0, 200000) + "\n[truncated]" : t;
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
write_file: async function (a) {
|
|
53
|
-
const x = safe(a.path);
|
|
54
|
-
await mkdir(dirname(x), { recursive: true });
|
|
55
|
-
await writeFile(x, a.content || "", "utf8");
|
|
56
|
-
return "wrote " + (a.content || "").length + " bytes to " + a.path;
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
edit_file: async function (a) {
|
|
60
|
-
const x = safe(a.path);
|
|
61
|
-
const orig = await readFile(x, "utf8");
|
|
62
|
-
const n = orig.split(a.old_string).length - 1;
|
|
63
|
-
if (n === 0) return "old_string not found in " + a.path;
|
|
64
|
-
if (n > 1 && !a.replace_all) return "old_string appears " + n + " times";
|
|
65
|
-
const next = a.replace_all
|
|
66
|
-
? orig.split(a.old_string).join(a.new_string)
|
|
67
|
-
: orig.replace(a.old_string, a.new_string);
|
|
68
|
-
await writeFile(x, next, "utf8");
|
|
69
|
-
return "edited " + a.path;
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
list_files: async function (a) {
|
|
73
|
-
const e = await readdir(safe(a.path || "."), { withFileTypes: true });
|
|
74
|
-
if (!e.length) return "EMPTY. Do not call list_files again.";
|
|
75
|
-
const names = e.map(function (x) {
|
|
76
|
-
return (x.isDirectory() ? "[dir] " : " ") + x.name;
|
|
77
|
-
});
|
|
78
|
-
names.sort();
|
|
79
|
-
return names.join("\n");
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
grep_search: async function (a) {
|
|
83
|
-
let re;
|
|
84
|
-
try {
|
|
85
|
-
re = new RegExp(a.pattern, a.case_sensitive ? "" : "i");
|
|
86
|
-
} catch (e) {
|
|
87
|
-
return "bad regex: " + e.message;
|
|
88
|
-
}
|
|
89
|
-
const hits = [];
|
|
90
|
-
for await (const f of walk(ws)) {
|
|
91
|
-
if (hits.length >= 80) break;
|
|
92
|
-
let info;
|
|
93
|
-
try {
|
|
94
|
-
info = await stat(f);
|
|
95
|
-
} catch (e) {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (info.size > 512000) continue;
|
|
99
|
-
let t;
|
|
100
|
-
try {
|
|
101
|
-
t = await readFile(f, "utf8");
|
|
102
|
-
} catch (e) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const lines = t.split("\n");
|
|
106
|
-
for (let i = 0; i < lines.length && hits.length < 80; i++) {
|
|
107
|
-
if (re.test(lines[i])) {
|
|
108
|
-
const rel = relative(ws, f).split(sep).join("/");
|
|
109
|
-
hits.push(rel + ":" + (i + 1) + ":" + lines[i].trim().slice(0, 200));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return hits.length ? hits.join("\n") : "no matches";
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
shell: function (a) {
|
|
117
|
-
const cmd = String(a.command || "");
|
|
118
|
-
if (!cmd) return Promise.resolve("shell requires command");
|
|
119
|
-
return new Promise(function (done) {
|
|
120
|
-
const c = spawn(cmd, { cwd: ws, shell: true, windowsHide: true });
|
|
121
|
-
let o = "";
|
|
122
|
-
let e = "";
|
|
123
|
-
if (c.stdout) c.stdout.on("data", function (d) { o += String(d); });
|
|
124
|
-
if (c.stderr) c.stderr.on("data", function (d) { e += String(d); });
|
|
125
|
-
const to = setTimeout(function () {
|
|
126
|
-
c.kill();
|
|
127
|
-
done("timed out after 60s");
|
|
128
|
-
}, 60000);
|
|
129
|
-
c.on("error", function (err) {
|
|
130
|
-
clearTimeout(to);
|
|
131
|
-
done("error: " + err.message);
|
|
132
|
-
});
|
|
133
|
-
c.on("close", function (code) {
|
|
134
|
-
clearTimeout(to);
|
|
135
|
-
const both = [o.trim(), e.trim()].filter(Boolean).join("\n");
|
|
136
|
-
let s = "exit " + code + "\n" + (both || "(no output)");
|
|
137
|
-
if (s.length > 65000) s = s.slice(0, 65000) + "\n[truncated]";
|
|
138
|
-
done(s);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export const CATALOG = [
|
|
145
|
-
"- read_file(path, start_line?, end_line?)",
|
|
146
|
-
"- write_file(path, content)",
|
|
147
|
-
"- edit_file(path, old_string, new_string, replace_all?)",
|
|
148
|
-
"- list_files(path?)",
|
|
149
|
-
"- grep_search(pattern, case_sensitive?)",
|
|
150
|
-
"- shell(command)"
|
|
151
|
-
].join("\n");
|