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/repl.mjs
CHANGED
|
@@ -4,16 +4,12 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { launch } from "./browser.mjs";
|
|
5
5
|
import { getProvider, listProviders } from "./providers.mjs";
|
|
6
6
|
import { waitForComposer } from "./wait.mjs";
|
|
7
|
+
import { enableDeepThink } from "./deepthink.mjs";
|
|
7
8
|
import { runTurn } from "./turn.mjs";
|
|
8
9
|
import { setWorkspace, getWorkspace } from "./tools/path.mjs";
|
|
9
10
|
import { setIgnore } from "./tools/walk.mjs";
|
|
10
11
|
import { loadInstructions, activeInstructionFile } from "./instructions.mjs";
|
|
11
|
-
import {
|
|
12
|
-
newSessionId,
|
|
13
|
-
saveSession,
|
|
14
|
-
loadSession,
|
|
15
|
-
listSessions
|
|
16
|
-
} from "./sessions.mjs";
|
|
12
|
+
import { newSessionId, saveSession, loadSession, listSessions } from "./sessions.mjs";
|
|
17
13
|
import { theme } from "./theme.mjs";
|
|
18
14
|
import { info, warn, error, ok } from "./log.mjs";
|
|
19
15
|
|
|
@@ -22,34 +18,28 @@ export async function runRepl(config) {
|
|
|
22
18
|
setWorkspace(config.ws);
|
|
23
19
|
setIgnore(config.ignored);
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
config.instructionsText = instructionsText;
|
|
21
|
+
config.instructionsText = await loadInstructions(config.ws);
|
|
27
22
|
|
|
28
|
-
// Session handling
|
|
29
23
|
let session;
|
|
30
24
|
if (config.resume) {
|
|
31
25
|
session = await loadSession(config.resume);
|
|
32
26
|
if (!session) {
|
|
33
|
-
warn("session not found
|
|
27
|
+
warn("session not found. starting fresh.");
|
|
34
28
|
session = newSession(config);
|
|
35
29
|
}
|
|
36
30
|
} else {
|
|
37
31
|
session = newSession(config);
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
// Banner
|
|
41
34
|
process.stdout.write("\n");
|
|
42
|
-
process.stdout.write(theme.bold(theme.cyan("Portal")) + theme.dim("
|
|
35
|
+
process.stdout.write(theme.bold(theme.cyan("Portal")) + theme.dim(" v3.0.2") + "\n");
|
|
43
36
|
process.stdout.write(theme.dim("workspace: ") + theme.accent(getWorkspace()) + "\n");
|
|
44
37
|
process.stdout.write(theme.dim("provider: ") + theme.accent(provider.label) + "\n");
|
|
45
38
|
process.stdout.write(theme.dim("session: ") + theme.accent(session.id) + "\n");
|
|
46
|
-
if (instructionsText) {
|
|
39
|
+
if (config.instructionsText) {
|
|
47
40
|
process.stdout.write(theme.dim("instructions: ") +
|
|
48
41
|
theme.accent(activeInstructionFile() || "?") + "\n");
|
|
49
42
|
}
|
|
50
|
-
if (config.yolo) {
|
|
51
|
-
process.stdout.write(theme.warn("yolo mode: all tool calls auto-approved") + "\n");
|
|
52
|
-
}
|
|
53
43
|
process.stdout.write("\n");
|
|
54
44
|
|
|
55
45
|
const ctx = await launch();
|
|
@@ -68,11 +58,15 @@ export async function runRepl(config) {
|
|
|
68
58
|
|
|
69
59
|
await waitForComposer(page, provider, config.loginTimeout);
|
|
70
60
|
|
|
61
|
+
if (provider.deepthink) {
|
|
62
|
+
const on = await enableDeepThink(page);
|
|
63
|
+
if (on) process.stdout.write(theme.green("+ DeepThink enabled") + "\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
71
66
|
ok(provider.label + " ready");
|
|
72
67
|
process.stdout.write(theme.dim("type /help for commands") + "\n");
|
|
73
68
|
process.stdout.write("\n");
|
|
74
69
|
|
|
75
|
-
// REPL
|
|
76
70
|
const rl = createInterface({
|
|
77
71
|
input: process.stdin,
|
|
78
72
|
output: process.stdout,
|
|
@@ -104,16 +98,21 @@ export async function runRepl(config) {
|
|
|
104
98
|
const t = line.trim();
|
|
105
99
|
if (!t) { rl.prompt(); return; }
|
|
106
100
|
|
|
107
|
-
// Slash commands
|
|
108
101
|
if (t === "/quit" || t === "/exit") { rl.close(); return; }
|
|
109
|
-
if (t === "/help"
|
|
102
|
+
if (t === "/help") { help(); rl.prompt(); return; }
|
|
110
103
|
if (t === "/pwd") { process.stdout.write(getWorkspace() + "\n"); rl.prompt(); return; }
|
|
111
104
|
if (t === "/queue") { showQueue(queue); rl.prompt(); return; }
|
|
112
105
|
if (t === "/status") { showStatus(config, session, queue, busy); rl.prompt(); return; }
|
|
113
|
-
if (t === "/history") { showHistory(session); rl.prompt(); return; }
|
|
114
106
|
if (t === "/clear") { clearScreen(); rl.prompt(); return; }
|
|
115
|
-
if (t === "/
|
|
107
|
+
if (t === "/deepthink") {
|
|
108
|
+
const on = await enableDeepThink(page);
|
|
109
|
+
process.stdout.write((on ? theme.ok("+") : theme.warn("!")) +
|
|
110
|
+
" DeepThink " + (on ? "on" : "not found") + "\n");
|
|
111
|
+
rl.prompt();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
116
114
|
if (t === "/providers") { showProviders(); rl.prompt(); return; }
|
|
115
|
+
if (t === "/sessions") { await showSessions(); rl.prompt(); return; }
|
|
117
116
|
if (t.slice(0, 4) === "/cd ") { changeDir(t.slice(4).trim()); rl.prompt(); return; }
|
|
118
117
|
if (t.slice(0, 10) === "/provider ") {
|
|
119
118
|
await changeProvider(t.slice(10).trim(), page, config, provider);
|
|
@@ -121,10 +120,9 @@ export async function runRepl(config) {
|
|
|
121
120
|
return;
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
// Queue or execute
|
|
125
123
|
if (busy) {
|
|
126
124
|
queue.push(t);
|
|
127
|
-
process.stdout.write(theme.dim("queued (" + queue.length + "
|
|
125
|
+
process.stdout.write(theme.dim("queued (" + queue.length + ")") + "\n");
|
|
128
126
|
rl.prompt();
|
|
129
127
|
return;
|
|
130
128
|
}
|
|
@@ -134,9 +132,7 @@ export async function runRepl(config) {
|
|
|
134
132
|
|
|
135
133
|
rl.on("close", async function () {
|
|
136
134
|
process.stdout.write("\n");
|
|
137
|
-
process.stdout.write(theme.dim("saving session...") + "\n");
|
|
138
135
|
await saveSession(session.id, session).catch(function () {});
|
|
139
|
-
process.stdout.write(theme.dim("closing browser...") + "\n");
|
|
140
136
|
await ctx.close().catch(function () {});
|
|
141
137
|
process.stdout.write(theme.ok("+ done") + "\n");
|
|
142
138
|
process.exit(0);
|
|
@@ -155,7 +151,7 @@ function newSession(config) {
|
|
|
155
151
|
};
|
|
156
152
|
}
|
|
157
153
|
|
|
158
|
-
function
|
|
154
|
+
function help() {
|
|
159
155
|
const lines = [
|
|
160
156
|
"COMMANDS",
|
|
161
157
|
" /help show this",
|
|
@@ -163,15 +159,12 @@ function showHelp() {
|
|
|
163
159
|
" /pwd show current workspace",
|
|
164
160
|
" /provider <name> switch provider",
|
|
165
161
|
" /providers list providers",
|
|
162
|
+
" /deepthink force DeepThink on",
|
|
166
163
|
" /sessions list saved sessions",
|
|
167
|
-
" /
|
|
168
|
-
" /
|
|
169
|
-
" /
|
|
170
|
-
" /
|
|
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."
|
|
164
|
+
" /queue list pending tasks",
|
|
165
|
+
" /status session status",
|
|
166
|
+
" /clear clear screen",
|
|
167
|
+
" /quit exit"
|
|
175
168
|
];
|
|
176
169
|
for (const l of lines) {
|
|
177
170
|
process.stdout.write((l.startsWith(" ") ? theme.dim(l) : theme.bold(l)) + "\n");
|
|
@@ -179,11 +172,8 @@ function showHelp() {
|
|
|
179
172
|
}
|
|
180
173
|
|
|
181
174
|
function showQueue(queue) {
|
|
182
|
-
if (!queue.length) {
|
|
183
|
-
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
process.stdout.write(theme.bold(queue.length + " task(s) waiting:") + "\n");
|
|
175
|
+
if (!queue.length) { process.stdout.write(theme.dim("(empty)") + "\n"); return; }
|
|
176
|
+
process.stdout.write(theme.bold(queue.length + " waiting:") + "\n");
|
|
187
177
|
queue.forEach(function (x, i) {
|
|
188
178
|
process.stdout.write(theme.dim(" " + (i + 1) + ". ") + x + "\n");
|
|
189
179
|
});
|
|
@@ -197,75 +187,46 @@ function showStatus(config, session, queue, busy) {
|
|
|
197
187
|
process.stdout.write(theme.dim(" turns: ") + session.turns.length + "\n");
|
|
198
188
|
process.stdout.write(theme.dim(" busy: ") + (busy ? "yes" : "no") + "\n");
|
|
199
189
|
process.stdout.write(theme.dim(" queued: ") + queue.length + "\n");
|
|
200
|
-
process.stdout.write(theme.dim(" yolo: ") + (config.yolo ? "on" : "off") + "\n");
|
|
201
190
|
}
|
|
202
191
|
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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");
|
|
192
|
+
function showProviders() {
|
|
193
|
+
process.stdout.write(theme.bold("providers:") + "\n");
|
|
194
|
+
const list = listProviders();
|
|
195
|
+
for (const p of list) {
|
|
196
|
+
process.stdout.write(theme.dim(" " + p.id.padEnd(10)) + " " + p.label + "\n");
|
|
213
197
|
}
|
|
214
198
|
}
|
|
215
199
|
|
|
216
200
|
async function showSessions() {
|
|
217
201
|
const list = await listSessions();
|
|
218
|
-
if (!list.length) {
|
|
219
|
-
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
process.stdout.write(theme.bold("saved sessions:") + "\n");
|
|
202
|
+
if (!list.length) { process.stdout.write(theme.dim("(none)") + "\n"); return; }
|
|
203
|
+
process.stdout.write(theme.bold("sessions:") + "\n");
|
|
223
204
|
for (const s of list.slice(0, 15)) {
|
|
224
|
-
|
|
225
|
-
|
|
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");
|
|
205
|
+
process.stdout.write(theme.dim(" " + s.id + " " + s.provider +
|
|
206
|
+
" " + s.turns + " turns") + "\n");
|
|
235
207
|
}
|
|
236
208
|
}
|
|
237
209
|
|
|
238
210
|
function changeDir(target) {
|
|
239
211
|
if (!target) {
|
|
240
212
|
process.stdout.write(theme.dim("usage: /cd <folder>") + "\n");
|
|
241
|
-
process.stdout.write(theme.dim("current: ") + getWorkspace() + "\n");
|
|
242
213
|
return;
|
|
243
214
|
}
|
|
244
215
|
const clean = target.replace(/^"|"$/g, "");
|
|
245
216
|
const abs = resolve(clean);
|
|
246
|
-
if (!existsSync(abs)) {
|
|
247
|
-
warn("not found: " + abs);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
217
|
+
if (!existsSync(abs)) { warn("not found: " + abs); return; }
|
|
250
218
|
setWorkspace(abs);
|
|
251
219
|
ok("workspace: " + getWorkspace());
|
|
252
220
|
}
|
|
253
221
|
|
|
254
222
|
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
|
-
}
|
|
223
|
+
if (name === current.id) return;
|
|
259
224
|
let next;
|
|
260
|
-
try {
|
|
261
|
-
next = getProvider(name);
|
|
262
|
-
} catch (e) {
|
|
263
|
-
error(e.message);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
225
|
+
try { next = getProvider(name); } catch (e) { error(e.message); return; }
|
|
266
226
|
info("switching to " + next.label + "...");
|
|
267
227
|
await page.goto(next.url);
|
|
268
228
|
await waitForComposer(page, next, 60000);
|
|
229
|
+
if (next.deepthink) await enableDeepThink(page);
|
|
269
230
|
config.provider = name;
|
|
270
231
|
ok("switched to " + next.label);
|
|
271
232
|
}
|
|
@@ -274,13 +235,3 @@ function clearScreen() {
|
|
|
274
235
|
process.stdout.write(String.fromCharCode(27) + "[2J");
|
|
275
236
|
process.stdout.write(String.fromCharCode(27) + "[H");
|
|
276
237
|
}
|
|
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
|
-
}
|
package/lib/safety.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { theme } from "./theme.mjs";
|
|
3
3
|
|
|
4
|
-
// Tools that touch the filesystem destructively or execute code. These
|
|
5
|
-
// require explicit approval unless --yolo is set.
|
|
6
4
|
export const SENSITIVE = new Set([
|
|
7
5
|
"shell",
|
|
8
6
|
"delete_file",
|
|
@@ -11,15 +9,14 @@ export const SENSITIVE = new Set([
|
|
|
11
9
|
"edit_file"
|
|
12
10
|
]);
|
|
13
11
|
|
|
14
|
-
// Read-only tools. Never prompt.
|
|
15
12
|
export const SAFE = new Set([
|
|
16
13
|
"read_file",
|
|
17
14
|
"list_files",
|
|
18
15
|
"find_files",
|
|
19
16
|
"grep_search",
|
|
20
|
-
"stat_file",
|
|
21
17
|
"git_status",
|
|
22
|
-
"git_diff"
|
|
18
|
+
"git_diff",
|
|
19
|
+
"git_log"
|
|
23
20
|
]);
|
|
24
21
|
|
|
25
22
|
export function isSensitive(name) {
|
|
@@ -30,7 +27,6 @@ export function isSafe(name) {
|
|
|
30
27
|
return SAFE.has(name);
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
// Approval prompt. Reads a single key from stdin.
|
|
34
30
|
export async function askApproval(toolName, args) {
|
|
35
31
|
const preview = JSON.stringify(args || {}, null, 2).slice(0, 500);
|
|
36
32
|
process.stdout.write("\n");
|
package/lib/send.mjs
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
|
-
import { sleep, norm, findFirstVisible
|
|
1
|
+
import { sleep, norm, findFirstVisible } from "./wait.mjs";
|
|
2
2
|
import { dismissPaste, composerEmpty } from "./paste.mjs";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { theme } from "./theme.mjs";
|
|
4
|
+
|
|
5
|
+
const SETTLE_MS = 1500;
|
|
6
|
+
const POLL_MS = 150;
|
|
7
|
+
|
|
8
|
+
function makeStreamer() {
|
|
9
|
+
let printed = 0;
|
|
10
|
+
let lastLength = 0;
|
|
11
|
+
return function (text) {
|
|
12
|
+
if (!text) return;
|
|
13
|
+
const t = String(text);
|
|
14
|
+
if (t.length > lastLength) {
|
|
15
|
+
const delta = t.slice(printed);
|
|
16
|
+
process.stdout.write(delta);
|
|
17
|
+
printed = t.length;
|
|
18
|
+
lastLength = t.length;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
export async function send(page, provider, prompt, timeoutMs) {
|
|
7
24
|
const input = await findFirstVisible(page, provider.input, 30000);
|
|
8
25
|
if (!input) throw new Error("composer not found");
|
|
9
26
|
|
|
10
|
-
const before = await countMatching(page, provider.response);
|
|
11
27
|
const want = norm(prompt);
|
|
12
28
|
|
|
13
29
|
await input.focus();
|
|
14
30
|
try {
|
|
15
31
|
await page.keyboard.press("Control+A");
|
|
16
32
|
await page.keyboard.press("Delete");
|
|
17
|
-
} catch (e) {
|
|
18
|
-
// empty composer
|
|
19
|
-
}
|
|
33
|
+
} catch (e) {}
|
|
20
34
|
|
|
21
35
|
await page.keyboard.insertText(prompt);
|
|
22
36
|
await sleep(300);
|
|
@@ -24,9 +38,7 @@ export async function send(page, provider, prompt, timeoutMs) {
|
|
|
24
38
|
|
|
25
39
|
try {
|
|
26
40
|
await input.press("Enter");
|
|
27
|
-
} catch (e) {
|
|
28
|
-
// fall through
|
|
29
|
-
}
|
|
41
|
+
} catch (e) {}
|
|
30
42
|
|
|
31
43
|
let ok = await composerEmpty(input, 5000);
|
|
32
44
|
if (!ok) {
|
|
@@ -39,9 +51,8 @@ export async function send(page, provider, prompt, timeoutMs) {
|
|
|
39
51
|
|
|
40
52
|
if (!ok) {
|
|
41
53
|
throw new Error(
|
|
42
|
-
"Message stayed in composer
|
|
43
|
-
"
|
|
44
|
-
"once manually, then resend."
|
|
54
|
+
"Message stayed in composer. Check Chrome: if a Cancel/Send bar " +
|
|
55
|
+
"is showing, click Send once manually, then resend."
|
|
45
56
|
);
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -53,40 +64,36 @@ async function waitForReply(page, provider, want, timeoutMs) {
|
|
|
53
64
|
let last = "";
|
|
54
65
|
let stab = Date.now();
|
|
55
66
|
let sawAnything = false;
|
|
56
|
-
|
|
67
|
+
const stream = makeStreamer();
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
const count = await countMatching(page, provider.response);
|
|
60
|
-
if (count > lastCount) lastCount = count;
|
|
69
|
+
process.stdout.write("\n" + theme.cyan("* ") + " ");
|
|
61
70
|
|
|
71
|
+
while (Date.now() < end) {
|
|
62
72
|
const node = await findFirstVisible(page, provider.response, 1000);
|
|
63
73
|
if (node) {
|
|
64
74
|
let tx = "";
|
|
65
75
|
try { tx = (await node.textContent()) || ""; } catch (e) { tx = ""; }
|
|
66
76
|
const n = norm(tx);
|
|
67
77
|
|
|
68
|
-
// Skip empty nodes and the echoed prompt.
|
|
69
78
|
if (!n || n === want) {
|
|
70
|
-
await sleep(
|
|
79
|
+
await sleep(POLL_MS);
|
|
71
80
|
continue;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
if (tx !== last) {
|
|
84
|
+
stream(tx);
|
|
75
85
|
last = tx;
|
|
76
86
|
stab = Date.now();
|
|
77
87
|
sawAnything = true;
|
|
78
88
|
} else if (sawAnything && Date.now() - stab > SETTLE_MS) {
|
|
89
|
+
process.stdout.write("\n\n");
|
|
79
90
|
return tx;
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
|
-
|
|
83
|
-
await sleep(300);
|
|
93
|
+
await sleep(POLL_MS);
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
process.stdout.write("\n\n");
|
|
86
97
|
if (last) return last;
|
|
87
|
-
throw new Error(
|
|
88
|
-
"Timed out waiting for a reply.\n" +
|
|
89
|
-
"Check the Chrome window. If the model answered but Portal missed it, " +
|
|
90
|
-
"send the task again."
|
|
91
|
-
);
|
|
98
|
+
throw new Error("Timed out waiting for a reply. Check the Chrome window.");
|
|
92
99
|
}
|
package/lib/sessions.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, writeFile
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
|
|
@@ -54,7 +54,6 @@ export async function listSessions() {
|
|
|
54
54
|
if (!s) continue;
|
|
55
55
|
metas.push({
|
|
56
56
|
id: id,
|
|
57
|
-
ws: s.ws || "",
|
|
58
57
|
provider: s.provider || "",
|
|
59
58
|
turns: (s.turns || []).length,
|
|
60
59
|
updatedAt: s.updatedAt || 0
|
|
@@ -63,13 +62,3 @@ export async function listSessions() {
|
|
|
63
62
|
metas.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
|
|
64
63
|
return metas;
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
export async function deleteSession(id) {
|
|
68
|
-
const path = join(dir(), id + ".json");
|
|
69
|
-
try {
|
|
70
|
-
await unlink(path);
|
|
71
|
-
return true;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
package/lib/system.mjs
CHANGED
|
@@ -2,21 +2,28 @@ import { CATALOG } from "./tools/index.mjs";
|
|
|
2
2
|
|
|
3
3
|
export function buildSystem(ws, instructions) {
|
|
4
4
|
const shown = ws.split("\\").join("/");
|
|
5
|
+
const isWin = process.platform === "win32";
|
|
5
6
|
const parts = [];
|
|
6
7
|
|
|
7
8
|
parts.push([
|
|
8
9
|
"You are a coding agent working in a terminal.",
|
|
9
10
|
"Workspace: " + shown,
|
|
11
|
+
"Platform: " + (isWin ? "Windows" : process.platform),
|
|
10
12
|
"",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
13
|
+
"Think carefully about each step before acting. Explain your reasoning",
|
|
14
|
+
"when it helps the user follow along. Take as much space as the task",
|
|
15
|
+
"requires.",
|
|
16
|
+
"",
|
|
17
|
+
"In file paths inside tool arguments, use forward slashes (L:/this/file.txt).",
|
|
18
|
+
isWin
|
|
19
|
+
? "In shell commands on Windows, use backslashes (del L:\\this\\file.txt)."
|
|
20
|
+
: "In shell commands, forward slashes are fine.",
|
|
21
|
+
"",
|
|
22
|
+
"Prefer purpose-built tools over shell. Use delete_file, not rm or del.",
|
|
23
|
+
"Do not call the same tool with the same arguments twice.",
|
|
24
|
+
"Read a file before editing it.",
|
|
25
|
+
"If a request is ambiguous, ask the user a real question.",
|
|
26
|
+
"When the task is complete, say so plainly and stop calling tools."
|
|
20
27
|
].join("\n"));
|
|
21
28
|
|
|
22
29
|
if (instructions) parts.push(instructions);
|
|
@@ -28,8 +35,7 @@ export function buildSystem(ws, instructions) {
|
|
|
28
35
|
"portal-tool: {\"tool\":\"name\",\"args\":{\"key\":\"value\"}}",
|
|
29
36
|
"",
|
|
30
37
|
"The JSON must use double quotes. Plain text only, no backticks.",
|
|
31
|
-
"
|
|
32
|
-
"Multiple calls in one reply are allowed, one per line."
|
|
38
|
+
"You may write narrative text around the tool calls."
|
|
33
39
|
].join("\n"));
|
|
34
40
|
|
|
35
41
|
parts.push("Available tools:\n" + CATALOG);
|
package/lib/theme.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const E = String.fromCharCode(27);
|
|
2
|
-
|
|
3
2
|
const MODE = process.env.NO_COLOR ? "plain" : "color";
|
|
4
|
-
const FORCE = process.env.PORTAL_THEME || "auto";
|
|
5
3
|
|
|
6
4
|
function c(code) {
|
|
7
5
|
if (MODE === "plain") return "";
|
|
@@ -40,17 +38,9 @@ export const theme = {
|
|
|
40
38
|
cyan: function (s) { return wrap(BCYN, s); },
|
|
41
39
|
accent: function (s) { return wrap(CYN, s); },
|
|
42
40
|
user: function (s) { return wrap(BLU, s); },
|
|
43
|
-
tool: function (s) { return wrap(MAG, s); }
|
|
44
|
-
italic: function (s) { return c(3) + String(s) + R; },
|
|
45
|
-
underline: function (s) { return c(4) + String(s) + R; }
|
|
41
|
+
tool: function (s) { return wrap(MAG, s); }
|
|
46
42
|
};
|
|
47
43
|
|
|
48
|
-
export function isDark() {
|
|
49
|
-
if (FORCE === "dark") return true;
|
|
50
|
-
if (FORCE === "light") return false;
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
44
|
export function colorEnabled() {
|
|
55
45
|
return MODE === "color";
|
|
56
46
|
}
|
package/lib/tools/delete.mjs
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat, rename } from "node:fs/promises";
|
|
2
2
|
import { safePath } from "./path.mjs";
|
|
3
3
|
|
|
4
|
-
// Move a file to a trash directory instead of unlinking. Safer default.
|
|
5
4
|
export async function delete_file(args) {
|
|
6
5
|
const abs = safePath(args.path);
|
|
7
6
|
try {
|
|
8
7
|
const st = await stat(abs);
|
|
9
|
-
if (st.isDirectory())
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// Instead of deleting, rename to .portal-trash
|
|
13
|
-
const trash = abs + ".portal-trash";
|
|
14
|
-
await rename(abs, trash);
|
|
15
|
-
return "moved " + args.path + " to " + args.path + ".portal-trash";
|
|
8
|
+
if (st.isDirectory()) return "delete_file cannot remove directories";
|
|
9
|
+
await rename(abs, abs + ".portal-trash");
|
|
10
|
+
return "moved " + args.path + " to trash";
|
|
16
11
|
} catch (e) {
|
|
17
12
|
return "delete_file failed: " + (e.message || e);
|
|
18
13
|
}
|
package/lib/tools/edit.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { safePath } from "./path.mjs";
|
|
3
3
|
|
|
4
|
-
// Exact string replace. Refuses ambiguous edits, so the model cannot
|
|
5
|
-
// accidentally replace the wrong occurrence.
|
|
6
4
|
export async function edit_file(args) {
|
|
7
5
|
const abs = safePath(args.path);
|
|
8
6
|
const oldStr = String(args.old_string || "");
|
|
@@ -15,13 +13,12 @@ export async function edit_file(args) {
|
|
|
15
13
|
const count = orig.split(oldStr).length - 1;
|
|
16
14
|
|
|
17
15
|
if (count === 0) {
|
|
18
|
-
return "old_string not found in " + args.path +
|
|
19
|
-
"Read the file first
|
|
16
|
+
return "old_string not found in " + args.path +
|
|
17
|
+
". Read the file first to see exact contents.";
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
if (count > 1 && !args.replace_all) {
|
|
23
|
-
return "old_string appears " + count + " times
|
|
24
|
-
"Add more surrounding context to make it unique, or set replace_all:true.";
|
|
21
|
+
return "old_string appears " + count + " times. Add context or set replace_all:true.";
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
const next = args.replace_all
|
|
@@ -33,19 +30,17 @@ export async function edit_file(args) {
|
|
|
33
30
|
|
|
34
31
|
const delta = next.length - orig.length;
|
|
35
32
|
const noun = args.replace_all ? count + " occurrences" : "1 occurrence";
|
|
36
|
-
return "edited " + args.path + ":
|
|
37
|
-
|
|
33
|
+
return "edited " + args.path + ": " + noun + " (" +
|
|
34
|
+
(delta >= 0 ? "+" : "") + delta + " bytes)";
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
// Restore the last backup for a file, if one exists.
|
|
41
37
|
export async function undo_file(args) {
|
|
42
38
|
const abs = safePath(args.path);
|
|
43
|
-
const backup = abs + ".portal-bak";
|
|
44
39
|
try {
|
|
45
|
-
const content = await readFile(
|
|
40
|
+
const content = await readFile(abs + ".portal-bak", "utf8");
|
|
46
41
|
await writeFile(abs, content, "utf8");
|
|
47
|
-
return "restored " + args.path
|
|
42
|
+
return "restored " + args.path;
|
|
48
43
|
} catch (e) {
|
|
49
|
-
return "no backup
|
|
44
|
+
return "no backup for " + args.path;
|
|
50
45
|
}
|
|
51
46
|
}
|
package/lib/tools/find.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relative, sep } from "node:path";
|
|
2
2
|
import { walk } from "./walk.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { toPosix, getWorkspace } from "./path.mjs";
|
|
4
4
|
|
|
5
5
|
function globToRegex(glob) {
|
|
6
6
|
let re = "";
|
|
@@ -25,15 +25,12 @@ export async function find_files(args) {
|
|
|
25
25
|
const re = globToRegex(pattern);
|
|
26
26
|
const ws = getWorkspace();
|
|
27
27
|
const hits = [];
|
|
28
|
-
const MAX = 200;
|
|
29
28
|
|
|
30
29
|
for await (const f of walk(ws)) {
|
|
31
|
-
if (hits.length >=
|
|
30
|
+
if (hits.length >= 200) break;
|
|
32
31
|
const rel = toPosix(relative(ws, f));
|
|
33
32
|
const base = f.split(sep).pop();
|
|
34
|
-
if (re.test(rel) || re.test(base))
|
|
35
|
-
hits.push(rel);
|
|
36
|
-
}
|
|
33
|
+
if (re.test(rel) || re.test(base)) hits.push(rel);
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
if (!hits.length) return "no files matched: " + pattern;
|
package/lib/tools/git.mjs
CHANGED
|
@@ -29,7 +29,7 @@ function run(args) {
|
|
|
29
29
|
|
|
30
30
|
export async function git_status() {
|
|
31
31
|
const r = await run(["status", "--short", "--branch"]);
|
|
32
|
-
if (r.code !== 0) return "not a git repository
|
|
32
|
+
if (r.code !== 0) return "not a git repository";
|
|
33
33
|
return r.out.trim() || "working tree clean";
|
|
34
34
|
}
|
|
35
35
|
|
package/lib/tools/grep.mjs
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { relative
|
|
2
|
+
import { relative } from "node:path";
|
|
3
3
|
import { walk } from "./walk.mjs";
|
|
4
4
|
import { safePath, toPosix, getWorkspace } from "./path.mjs";
|
|
5
5
|
|
|
6
|
-
const MAX_RESULTS = 80;
|
|
7
|
-
const MAX_FILE_BYTES = 512000;
|
|
8
|
-
|
|
9
6
|
export async function grep_search(args) {
|
|
10
7
|
const pattern = String(args.pattern || "");
|
|
11
8
|
if (!pattern) return "grep_search requires a pattern";
|
|
@@ -22,16 +19,16 @@ export async function grep_search(args) {
|
|
|
22
19
|
const hits = [];
|
|
23
20
|
|
|
24
21
|
for await (const f of walk(startPath)) {
|
|
25
|
-
if (hits.length >=
|
|
22
|
+
if (hits.length >= 80) break;
|
|
26
23
|
let info;
|
|
27
24
|
try { info = await stat(f); } catch (e) { continue; }
|
|
28
|
-
if (info.size >
|
|
25
|
+
if (info.size > 512000) continue;
|
|
29
26
|
|
|
30
27
|
let text;
|
|
31
28
|
try { text = await readFile(f, "utf8"); } catch (e) { continue; }
|
|
32
29
|
const lines = text.split("\n");
|
|
33
30
|
|
|
34
|
-
for (let i = 0; i < lines.length && hits.length <
|
|
31
|
+
for (let i = 0; i < lines.length && hits.length < 80; i++) {
|
|
35
32
|
if (re.test(lines[i])) {
|
|
36
33
|
const rel = toPosix(relative(ws, f));
|
|
37
34
|
hits.push(rel + ":" + (i + 1) + ":" + lines[i].trim().slice(0, 200));
|
|
@@ -39,6 +36,6 @@ export async function grep_search(args) {
|
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
if (!hits.length) return "no matches
|
|
39
|
+
if (!hits.length) return "no matches";
|
|
43
40
|
return hits.length + " matches:\n" + hits.join("\n");
|
|
44
41
|
}
|