portal-agent-cli 3.0.0 → 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 -0
- package/README.md +15 -7
- package/lib/args.mjs +3 -1
- package/lib/browser.mjs +1 -3
- package/lib/config.mjs +0 -1
- package/lib/deepthink.mjs +55 -0
- package/lib/exec.mjs +1 -30
- package/lib/instructions.mjs +1 -44
- package/lib/parse.mjs +1 -50
- package/lib/paste.mjs +0 -2
- package/lib/providers.mjs +4 -4
- package/lib/repl.mjs +1 -283
- package/lib/safety.mjs +1 -64
- package/lib/send.mjs +30 -14
- package/lib/sessions.mjs +1 -75
- package/lib/system.mjs +1 -35
- package/lib/theme.mjs +0 -7
- package/lib/tools/delete.mjs +2 -30
- package/lib/tools/edit.mjs +1 -51
- package/lib/tools/find.mjs +1 -42
- package/lib/tools/git.mjs +1 -48
- package/lib/tools/grep.mjs +1 -43
- package/lib/tools/index.mjs +1 -52
- package/lib/tools/list.mjs +1 -38
- package/lib/tools/path.mjs +1 -22
- package/lib/tools/read.mjs +1 -30
- package/lib/tools/shell.mjs +1 -41
- package/lib/tools/walk.mjs +1 -32
- package/lib/tools/write.mjs +1 -42
- package/lib/turn.mjs +1 -117
- package/package.json +3 -2
- package/portal.mjs +1 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# portal-agent
|
|
2
2
|
|
|
3
|
-
Drive
|
|
3
|
+
Drive web AI products from the terminal with local tools. Uses a real
|
|
4
|
+
Chromium browser against the provider's own website. No
|
|
4
5
|
```
|
|
5
6
|
|
|
6
7
|
npm install -g portal-agent-cli
|
|
@@ -15,14 +16,15 @@ portal-agent --workspace F:/projects/my-app
|
|
|
15
16
|
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
Or without installing
|
|
19
|
+
Or without installing:
|
|
20
|
+
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
npx portal-agent-cli --workspace F:/projects/my-app
|
|
22
24
|
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
On first run, Chrome
|
|
27
|
+
On first run, Chrome
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
portal-agent --workspace F:/proj exec "list the files here"
|
|
@@ -32,7 +34,10 @@ portal-agent --workspace F:/proj exec "list the files here"
|
|
|
32
34
|
## Tools
|
|
33
35
|
|
|
34
36
|
| Tool | Description |
|
|
35
|
-
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `read_file(path, start_line?, end_line?)` | read with line numbers |
|
|
39
|
+
| `write_file(path, content)` | write a whole file (backs up existing) |
|
|
40
|
+
| `edit_file(path, old_string, new_string, replace_all?)`
|
|
36
41
|
```
|
|
37
42
|
|
|
38
43
|
- approval required
|
|
@@ -44,12 +49,15 @@ allow? yes / always / no:
|
|
|
44
49
|
|
|
45
50
|
```
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
Answering `a` approves that tool for the rest of the session.
|
|
53
|
+
|
|
54
|
+
## Safety
|
|
55
|
+
|
|
56
|
+
-
|
|
48
57
|
```
|
|
49
58
|
|
|
50
59
|
portal-tool: {"tool":"read_file","args":{"path":"notes.txt"}}
|
|
51
60
|
|
|
52
61
|
```
|
|
53
62
|
|
|
54
|
-
Portal parses that line, runs the tool locally, and sends
|
|
55
|
-
into the same conversation. This
|
|
63
|
+
Portal parses that line, runs the tool locally, and sends
|
package/lib/args.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export function parseArgs() {
|
|
|
35
35
|
else if (t === "--yolo") o.yolo = true;
|
|
36
36
|
else if (t === "exec") { o.exec = a.slice(i + 1).join(" "); break; }
|
|
37
37
|
else if (t === "--help" || t === "-h") { help(); process.exit(0); }
|
|
38
|
-
else if (t === "--version" || t === "-v") { console.log("
|
|
38
|
+
else if (t === "--version" || t === "-v") { console.log("3.0.1"); process.exit(0); }
|
|
39
39
|
i++;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -78,8 +78,10 @@ function help() {
|
|
|
78
78
|
console.log(" PORTAL_WORKSPACE same as --workspace");
|
|
79
79
|
console.log(" PORTAL_PROVIDER default provider");
|
|
80
80
|
console.log(" PORTAL_BROWSER_PATH explicit browser executable");
|
|
81
|
+
console.log(" PORTAL_DEEPTHINK set to off to skip DeepThink toggle");
|
|
81
82
|
console.log(" PORTAL_DEBUG print stack traces on error");
|
|
82
83
|
console.log(" PORTAL_YOLO same as --yolo");
|
|
84
|
+
console.log(" NO_COLOR disable color output");
|
|
83
85
|
console.log("");
|
|
84
86
|
console.log(t.bold("EXAMPLES"));
|
|
85
87
|
console.log(" portal-agent --workspace F:/projects/app");
|
package/lib/browser.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export function listBrowsers() {
|
|
|
38
38
|
return found;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export async function launch(
|
|
41
|
+
export async function launch() {
|
|
42
42
|
const exe = findBrowser();
|
|
43
43
|
if (!exe) {
|
|
44
44
|
throw new Error(
|
|
@@ -58,8 +58,6 @@ export async function launch(opts) {
|
|
|
58
58
|
args: ["--disable-blink-features=AutomationControlled"]
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
// Auto-dismiss dialogs (alert, confirm, prompt) so a stray modal cannot
|
|
62
|
-
// freeze the session.
|
|
63
61
|
ctx.on("page", function (page) {
|
|
64
62
|
page.on("dialog", function (d) {
|
|
65
63
|
d.dismiss().catch(function () {});
|
package/lib/config.mjs
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Toggle DeepSeek's DeepThink reasoning mode. Without this, DeepSeek answers
|
|
2
|
+
// with the fast model and the output is shorter and less thorough.
|
|
3
|
+
//
|
|
4
|
+
// The button sits in the composer row with a label like "DeepThink" or
|
|
5
|
+
// "Deep Think". After clicking, it gains an active class or aria-pressed=true.
|
|
6
|
+
|
|
7
|
+
export async function enableDeepThink(page) {
|
|
8
|
+
if (process.env.PORTAL_DEEPTHINK === "off") return false;
|
|
9
|
+
|
|
10
|
+
const candidates = page.locator("button, [role=button], div[role=button]");
|
|
11
|
+
let total = 0;
|
|
12
|
+
try {
|
|
13
|
+
total = await candidates.count();
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (total === 0 || total > 400) return false;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < total; i++) {
|
|
20
|
+
const btn = candidates.nth(i);
|
|
21
|
+
let text = "";
|
|
22
|
+
try {
|
|
23
|
+
text = ((await btn.textContent()) || "").trim();
|
|
24
|
+
} catch (e) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!text) continue;
|
|
28
|
+
|
|
29
|
+
const isDeepthink = text === "DeepThink" ||
|
|
30
|
+
text === "Deep Think" ||
|
|
31
|
+
text.indexOf("DeepThink") >= 0 ||
|
|
32
|
+
text.indexOf("Deep Think") >= 0;
|
|
33
|
+
|
|
34
|
+
if (!isDeepthink) continue;
|
|
35
|
+
|
|
36
|
+
let pressed = "";
|
|
37
|
+
let cls = "";
|
|
38
|
+
try { pressed = (await btn.getAttribute("aria-pressed")) || ""; } catch (e) {}
|
|
39
|
+
try { cls = (await btn.getAttribute("class")) || ""; } catch (e) {}
|
|
40
|
+
|
|
41
|
+
const alreadyOn = pressed === "true" ||
|
|
42
|
+
/active|selected|on\b|enabled/i.test(cls);
|
|
43
|
+
|
|
44
|
+
if (alreadyOn) return true;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await btn.click({ timeout: 2000 });
|
|
48
|
+
await new Promise(function (r) { setTimeout(r, 400); });
|
|
49
|
+
return true;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
package/lib/exec.mjs
CHANGED
|
@@ -1,32 +1,3 @@
|
|
|
1
1
|
import { launch } from "./browser.mjs";
|
|
2
2
|
import { getProvider } from "./providers.mjs";
|
|
3
|
-
import { waitForComposer } from "./wait
|
|
4
|
-
import { runTurn } from "./turn.mjs";
|
|
5
|
-
import { setWorkspace } from "./tools/path.mjs";
|
|
6
|
-
import { setIgnore } from "./tools/walk.mjs";
|
|
7
|
-
import { loadInstructions } from "./instructions.mjs";
|
|
8
|
-
import { info, error } from "./log.mjs";
|
|
9
|
-
|
|
10
|
-
export async function runExec(config) {
|
|
11
|
-
const provider = getProvider(config.provider);
|
|
12
|
-
setWorkspace(config.ws);
|
|
13
|
-
setIgnore(config.ignored);
|
|
14
|
-
|
|
15
|
-
config.instructionsText = await loadInstructions(config.ws);
|
|
16
|
-
|
|
17
|
-
const ctx = await launch();
|
|
18
|
-
const page = await ctx.newPage();
|
|
19
|
-
page.setDefaultTimeout(30000);
|
|
20
|
-
|
|
21
|
-
info("opening " + provider.label + "...");
|
|
22
|
-
await page.goto(provider.url);
|
|
23
|
-
await waitForComposer(page, provider, config.loginTimeout);
|
|
24
|
-
|
|
25
|
-
const session = {
|
|
26
|
-
id: "exec-" + Date.now(),
|
|
27
|
-
ws: config.ws,
|
|
28
|
-
provider: config.provider,
|
|
29
|
-
turns: [],
|
|
30
|
-
approvedAlways: [],
|
|
31
|
-
startedAt: Date.now(),
|
|
32
|
-
|
|
3
|
+
import { waitForComposer } from "./wait
|
package/lib/instructions.mjs
CHANGED
|
@@ -1,44 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
const FILENAMES = ["PORTAL.md", "AGENTS.md", ".portal.md", "CLAUDE.md"];
|
|
5
|
-
|
|
6
|
-
let cache = null;
|
|
7
|
-
let cacheKey = "";
|
|
8
|
-
|
|
9
|
-
export async function loadInstructions(ws) {
|
|
10
|
-
for (const name of FILENAMES) {
|
|
11
|
-
const path = join(ws, name);
|
|
12
|
-
let info;
|
|
13
|
-
try {
|
|
14
|
-
info = await stat(path);
|
|
15
|
-
} catch (e) {
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const key = path + ":" + info.mtimeMs;
|
|
20
|
-
if (cache && cacheKey === key) return cache;
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const content = await readFile(path, "utf8");
|
|
24
|
-
const trimmed = content.trim();
|
|
25
|
-
if (!trimmed) return null;
|
|
26
|
-
cache = [
|
|
27
|
-
"# Project instructions",
|
|
28
|
-
"",
|
|
29
|
-
"The user placed these instructions in " + name + ". Follow them.",
|
|
30
|
-
"",
|
|
31
|
-
trimmed
|
|
32
|
-
].join("\n");
|
|
33
|
-
cacheKey = key;
|
|
34
|
-
return cache;
|
|
35
|
-
} catch (e) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function activeInstructionFile() {
|
|
43
|
-
return cacheKey ? cacheKey.split(":")[0] : null;
|
|
44
|
-
}
|
|
1
|
+
import {
|
package/lib/parse.mjs
CHANGED
|
@@ -17,53 +17,4 @@ function findJsonEnd(s, i) {
|
|
|
17
17
|
if (esc) { esc = false; continue; }
|
|
18
18
|
if (c === "\\") { esc = true; continue; }
|
|
19
19
|
if (c === String.fromCharCode(34)) { inStr = !inStr; continue; }
|
|
20
|
-
if (inStr
|
|
21
|
-
if (c === "{") depth++;
|
|
22
|
-
else if (c === "}") {
|
|
23
|
-
depth--;
|
|
24
|
-
if (!depth) return j + 1;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return -1;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function repairBackslashes(raw) {
|
|
31
|
-
let s = raw;
|
|
32
|
-
const bad = ["\\t", "\\h", "\\w", "\\d", "\\p", "\\v", "\\x", "\\c"];
|
|
33
|
-
for (const b of bad) s = s.split(b).join("/");
|
|
34
|
-
return s;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function parseCalls(text) {
|
|
38
|
-
const out = [];
|
|
39
|
-
const seen = new Set();
|
|
40
|
-
const re = /portal-tool[:\s]*/g;
|
|
41
|
-
let m;
|
|
42
|
-
while ((m = re.exec(text))) {
|
|
43
|
-
const b = text.indexOf("{", m.index + m[0].length);
|
|
44
|
-
if (b < 0) continue;
|
|
45
|
-
const e = findJsonEnd(text, b);
|
|
46
|
-
if (e < 0) continue;
|
|
47
|
-
const raw = text.slice(b, e);
|
|
48
|
-
if (seen.has(raw)) continue;
|
|
49
|
-
seen.add(raw);
|
|
50
|
-
try {
|
|
51
|
-
out.push(JSON.parse(raw));
|
|
52
|
-
} catch (x) {
|
|
53
|
-
try {
|
|
54
|
-
out.push(JSON.parse(repairBackslashes(raw)));
|
|
55
|
-
} catch (y) {
|
|
56
|
-
// skip malformed
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return out;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function stripCalls(text) {
|
|
64
|
-
return text.replace(/portal-tool[\s\S]*?\n\}/g, "").trim();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function hasToolCall(text) {
|
|
68
|
-
return /portal-tool[:\s]*\{/.test(text);
|
|
69
|
-
}
|
|
20
|
+
if (inStr
|
package/lib/paste.mjs
CHANGED
|
@@ -53,8 +53,6 @@ export async function dismissPaste(page) {
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// Poll the composer for emptiness. A thrown inputValue() means the composer
|
|
57
|
-
// is a contenteditable with no inputValue method, so treat that as sent.
|
|
58
56
|
export async function composerEmpty(input, ms) {
|
|
59
57
|
const end = Date.now() + ms;
|
|
60
58
|
while (Date.now() < end) {
|
package/lib/providers.mjs
CHANGED
|
@@ -7,7 +7,7 @@ export const PROVIDERS = {
|
|
|
7
7
|
submit: ["button[aria-label=Send]", "button[data-testid=send-button]"],
|
|
8
8
|
response: ["div.ds-markdown", "div[class*=message]"],
|
|
9
9
|
paste: true,
|
|
10
|
-
|
|
10
|
+
deepthink: true
|
|
11
11
|
},
|
|
12
12
|
chatgpt: {
|
|
13
13
|
id: "chatgpt",
|
|
@@ -17,7 +17,7 @@ export const PROVIDERS = {
|
|
|
17
17
|
submit: ["button[data-testid=send-button]", "button[aria-label*=Send]"],
|
|
18
18
|
response: ["[data-message-author-role=assistant]", "article[data-testid^=conversation-turn]"],
|
|
19
19
|
paste: false,
|
|
20
|
-
|
|
20
|
+
deepthink: false
|
|
21
21
|
},
|
|
22
22
|
gemini: {
|
|
23
23
|
id: "gemini",
|
|
@@ -27,7 +27,7 @@ export const PROVIDERS = {
|
|
|
27
27
|
submit: ["button[aria-label*=Send]", "button[aria-label=Send message]"],
|
|
28
28
|
response: ["message-content", "div[class*=model-response-text]"],
|
|
29
29
|
paste: false,
|
|
30
|
-
|
|
30
|
+
deepthink: false
|
|
31
31
|
},
|
|
32
32
|
grok: {
|
|
33
33
|
id: "grok",
|
|
@@ -37,7 +37,7 @@ export const PROVIDERS = {
|
|
|
37
37
|
submit: ["button[type=submit]", "button[aria-label*=Send]"],
|
|
38
38
|
response: ["div[class*=message]", "div[class*=response]"],
|
|
39
39
|
paste: false,
|
|
40
|
-
|
|
40
|
+
deepthink: false
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
|
package/lib/repl.mjs
CHANGED
|
@@ -1,286 +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, listProviders } from "./providers.mjs";
|
|
6
|
-
import { waitForComposer } from "./wait.mjs";
|
|
7
|
-
import { runTurn } from "./turn.mjs";
|
|
8
|
-
import { setWorkspace, getWorkspace } from "./tools/path.mjs";
|
|
9
|
-
import { setIgnore } from "./tools/walk.mjs";
|
|
10
|
-
import { loadInstructions, activeInstructionFile } from "./instructions.mjs";
|
|
11
|
-
import {
|
|
12
|
-
newSessionId,
|
|
13
|
-
saveSession,
|
|
14
|
-
loadSession,
|
|
15
|
-
listSessions
|
|
16
|
-
} from "./sessions.mjs";
|
|
17
|
-
import { theme } from "./theme.mjs";
|
|
18
|
-
import { info, warn, error, ok } from "./log.mjs";
|
|
19
|
-
|
|
20
|
-
export async function runRepl(config) {
|
|
21
|
-
const provider = getProvider(config.provider);
|
|
22
|
-
setWorkspace(config.ws);
|
|
23
|
-
setIgnore(config.ignored);
|
|
24
|
-
|
|
25
|
-
const instructionsText = await loadInstructions(config.ws);
|
|
26
|
-
config.instructionsText = instructionsText;
|
|
27
|
-
|
|
28
|
-
// Session handling
|
|
29
|
-
let session;
|
|
30
|
-
if (config.resume) {
|
|
31
|
-
session = await loadSession(config.resume);
|
|
32
|
-
if (!session) {
|
|
33
|
-
warn("session not found: " + config.resume + ". starting fresh.");
|
|
34
|
-
session = newSession(config);
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
session = newSession(config);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Banner
|
|
41
|
-
process.stdout.write("\n");
|
|
42
|
-
process.stdout.write(theme.bold(theme.cyan("Portal")) + theme.dim(" v2.0.0") + "\n");
|
|
43
|
-
process.stdout.write(theme.dim("workspace: ") + theme.accent(getWorkspace()) + "\n");
|
|
44
|
-
process.stdout.write(theme.dim("provider: ") + theme.accent(provider.label) + "\n");
|
|
45
|
-
process.stdout.write(theme.dim("session: ") + theme.accent(session.id) + "\n");
|
|
46
|
-
if (instructionsText) {
|
|
47
|
-
process.stdout.write(theme.dim("instructions: ") +
|
|
48
|
-
theme.accent(activeInstructionFile() || "?") + "\n");
|
|
49
|
-
}
|
|
50
|
-
if (config.yolo) {
|
|
51
|
-
process.stdout.write(theme.warn("yolo mode: all tool calls auto-approved") + "\n");
|
|
52
|
-
}
|
|
53
|
-
process.stdout.write("\n");
|
|
54
|
-
|
|
55
|
-
const ctx = await launch();
|
|
56
|
-
const page = await ctx.newPage();
|
|
57
|
-
page.setDefaultTimeout(30000);
|
|
58
|
-
|
|
59
|
-
info("opening " + provider.label + "...");
|
|
60
|
-
await page.goto(provider.url);
|
|
61
|
-
|
|
62
|
-
process.stdout.write("\n");
|
|
63
|
-
process.stdout.write(theme.yellow("Log in to " + provider.label +
|
|
64
|
-
" in the Chrome window.") + "\n");
|
|
65
|
-
process.stdout.write(theme.dim("Waiting up to " +
|
|
66
|
-
Math.round(config.loginTimeout / 1000) + "s.") + "\n");
|
|
67
|
-
process.stdout.write("\n");
|
|
68
|
-
|
|
69
|
-
await waitForComposer(page, provider, config.loginTimeout);
|
|
70
|
-
|
|
71
|
-
ok(provider.label + " ready");
|
|
72
|
-
process.stdout.write(theme.dim("type /help for commands") + "\n");
|
|
73
|
-
process.stdout.write("\n");
|
|
74
|
-
|
|
75
|
-
// REPL
|
|
76
|
-
const rl = createInterface({
|
|
77
|
-
input: process.stdin,
|
|
78
|
-
output: process.stdout,
|
|
79
|
-
prompt: theme.user("> ")
|
|
80
|
-
});
|
|
81
|
-
rl.prompt();
|
|
82
|
-
|
|
83
|
-
let busy = false;
|
|
84
|
-
const queue = [];
|
|
85
|
-
|
|
86
|
-
async function drain() {
|
|
87
|
-
if (busy) return;
|
|
88
|
-
const next = queue.shift();
|
|
89
|
-
if (!next) return;
|
|
90
|
-
busy = true;
|
|
91
|
-
try {
|
|
92
|
-
await runTurn(page, provider, next, config, session);
|
|
93
|
-
await saveSession(session.id, session);
|
|
94
|
-
} catch (e) {
|
|
95
|
-
error(e.message || String(e));
|
|
96
|
-
}
|
|
97
|
-
busy = false;
|
|
98
|
-
process.stdout.write("\n");
|
|
99
|
-
rl.prompt();
|
|
100
|
-
if (queue.length > 0) drain();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
rl.on("line", async function (line) {
|
|
104
|
-
const t = line.trim();
|
|
105
|
-
if (!t) { rl.prompt(); return; }
|
|
106
|
-
|
|
107
|
-
// Slash commands
|
|
108
|
-
if (t === "/quit" || t === "/exit") { rl.close(); return; }
|
|
109
|
-
if (t === "/help" || t === "/?") { showHelp(); rl.prompt(); return; }
|
|
110
|
-
if (t === "/pwd") { process.stdout.write(getWorkspace() + "\n"); rl.prompt(); return; }
|
|
111
|
-
if (t === "/queue") { showQueue(queue); rl.prompt(); return; }
|
|
112
|
-
if (t === "/status") { showStatus(config, session, queue, busy); rl.prompt(); return; }
|
|
113
|
-
if (t === "/history") { showHistory(session); rl.prompt(); return; }
|
|
114
|
-
if (t === "/clear") { clearScreen(); rl.prompt(); return; }
|
|
115
|
-
if (t === "/sessions") { await showSessions(); rl.prompt(); return; }
|
|
116
|
-
if (t === "/providers") { showProviders(); rl.prompt(); return; }
|
|
117
|
-
if (t.slice(0, 4) === "/cd ") { changeDir(t.slice(4).trim()); rl.prompt(); return; }
|
|
118
|
-
if (t.slice(0, 10) === "/provider ") {
|
|
119
|
-
await changeProvider(t.slice(10).trim(), page, config, provider);
|
|
120
|
-
rl.prompt();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Queue or execute
|
|
125
|
-
if (busy) {
|
|
126
|
-
queue.push(t);
|
|
127
|
-
process.stdout.write(theme.dim("queued (" + queue.length + " waiting)") + "\n");
|
|
128
|
-
rl.prompt();
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
queue.push(t);
|
|
132
|
-
drain();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
rl.on("close", async function () {
|
|
136
|
-
process.stdout.write("\n");
|
|
137
|
-
process.stdout.write(theme.dim("saving session...") + "\n");
|
|
138
|
-
await saveSession(session.id, session).catch(function () {});
|
|
139
|
-
process.stdout.write(theme.dim("closing browser...") + "\n");
|
|
140
|
-
await ctx.close().catch(function () {});
|
|
141
|
-
process.stdout.write(theme.ok("+ done") + "\n");
|
|
142
|
-
process.exit(0);
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function newSession(config) {
|
|
147
|
-
return {
|
|
148
|
-
id: newSessionId(),
|
|
149
|
-
ws: config.ws,
|
|
150
|
-
provider: config.provider,
|
|
151
|
-
turns: [],
|
|
152
|
-
approvedAlways: [],
|
|
153
|
-
startedAt: Date.now(),
|
|
154
|
-
updatedAt: Date.now()
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function showHelp() {
|
|
159
|
-
const lines = [
|
|
160
|
-
"COMMANDS",
|
|
161
|
-
" /help show this",
|
|
162
|
-
" /cd <folder> change workspace",
|
|
163
|
-
" /pwd show current workspace",
|
|
164
|
-
" /provider <name> switch provider",
|
|
165
|
-
" /providers list providers",
|
|
166
|
-
" /sessions list saved sessions",
|
|
167
|
-
" /history show recent turns",
|
|
168
|
-
" /queue list tasks waiting to run",
|
|
169
|
-
" /status show session status",
|
|
170
|
-
" /clear clear the screen",
|
|
171
|
-
" /quit exit",
|
|
172
|
-
"",
|
|
173
|
-
"Anything else is sent to the model as a task.",
|
|
174
|
-
"Tasks typed while the model is busy are queued."
|
|
175
|
-
];
|
|
176
|
-
for (const l of lines) {
|
|
177
|
-
process.stdout.write((l.startsWith(" ") ? theme.dim(l) : theme.bold(l)) + "\n");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function showQueue(queue) {
|
|
182
|
-
if (!queue.length) {
|
|
183
|
-
process.stdout.write(theme.dim("(queue empty)") + "\n");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
process.stdout.write(theme.bold(queue.length + " task(s) waiting:") + "\n");
|
|
187
|
-
queue.forEach(function (x, i) {
|
|
188
|
-
process.stdout.write(theme.dim(" " + (i + 1) + ". ") + x + "\n");
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function showStatus(config, session, queue, busy) {
|
|
193
|
-
process.stdout.write(theme.bold("session status") + "\n");
|
|
194
|
-
process.stdout.write(theme.dim(" id: ") + session.id + "\n");
|
|
195
|
-
process.stdout.write(theme.dim(" workspace: ") + getWorkspace() + "\n");
|
|
196
|
-
process.stdout.write(theme.dim(" provider: ") + session.provider + "\n");
|
|
197
|
-
process.stdout.write(theme.dim(" turns: ") + session.turns.length + "\n");
|
|
198
|
-
process.stdout.write(theme.dim(" busy: ") + (busy ? "yes" : "no") + "\n");
|
|
199
|
-
process.stdout.write(theme.dim(" queued: ") + queue.length + "\n");
|
|
200
|
-
process.stdout.write(theme.dim(" yolo: ") + (config.yolo ? "on" : "off") + "\n");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function showHistory(session) {
|
|
204
|
-
if (!session.turns.length) {
|
|
205
|
-
process.stdout.write(theme.dim("(no turns yet)") + "\n");
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
const recent = session.turns.slice(-10);
|
|
209
|
-
for (const t of recent) {
|
|
210
|
-
const tag = t.role === "user" ? theme.user("you") : theme.cyan("ai");
|
|
211
|
-
const text = String(t.text || "").slice(0, 200);
|
|
212
|
-
process.stdout.write(tag + ": " + text + "\n");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function showSessions() {
|
|
217
|
-
const list = await listSessions();
|
|
218
|
-
if (!list.length) {
|
|
219
|
-
process.stdout.write(theme.dim("(no saved sessions)") + "\n");
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
process.stdout.write(theme.bold("saved sessions:") + "\n");
|
|
223
|
-
for (const s of list.slice(0, 15)) {
|
|
224
|
-
const when = timeAgo(s.updatedAt);
|
|
225
|
-
process.stdout.write(theme.dim(" " + s.id + " " + when + " " +
|
|
226
|
-
s.provider + " " + s.turns + " turns") + "\n");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function showProviders() {
|
|
231
|
-
const list = listProviders();
|
|
232
|
-
process.stdout.write(theme.bold("providers:") + "\n");
|
|
233
|
-
for (const p of list) {
|
|
234
|
-
process.stdout.write(theme.dim(" " + p.id.padEnd(10)) + " " + p.label + "\n");
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function changeDir(target) {
|
|
239
|
-
if (!target) {
|
|
240
|
-
process.stdout.write(theme.dim("usage: /cd <folder>") + "\n");
|
|
241
|
-
process.stdout.write(theme.dim("current: ") + getWorkspace() + "\n");
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const clean = target.replace(/^"|"$/g, "");
|
|
245
|
-
const abs = resolve(clean);
|
|
246
|
-
if (!existsSync(abs)) {
|
|
247
|
-
warn("not found: " + abs);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
setWorkspace(abs);
|
|
251
|
-
ok("workspace: " + getWorkspace());
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function changeProvider(name, page, config, current) {
|
|
255
|
-
if (name === current.id) {
|
|
256
|
-
process.stdout.write(theme.dim("already on " + name) + "\n");
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
let next;
|
|
260
|
-
try {
|
|
261
|
-
next = getProvider(name);
|
|
262
|
-
} catch (e) {
|
|
263
|
-
error(e.message);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
info("switching to " + next.label + "...");
|
|
267
|
-
await page.goto(next.url);
|
|
268
|
-
await waitForComposer(page, next, 60000);
|
|
269
|
-
config.provider = name;
|
|
270
|
-
ok("switched to " + next.label);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function clearScreen() {
|
|
274
|
-
process.stdout.write(String.fromCharCode(27) + "[2J");
|
|
275
|
-
process.stdout.write(String.fromCharCode(27) + "[H");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function timeAgo(ms) {
|
|
279
|
-
const s = Math.floor((Date.now() - ms) / 1000);
|
|
280
|
-
if (s < 60) return s + "s ago";
|
|
281
|
-
const m = Math.floor(s / 60);
|
|
282
|
-
if (m < 60) return m + "m ago";
|
|
283
|
-
const h = Math.floor(m / 60);
|
|
284
|
-
if (h < 24) return h + "h ago";
|
|
285
|
-
return Math.floor(h / 24) + "d ago";
|
|
286
|
-
}
|
|
4
|
+
import { launch } from "./
|