agentlas 1.0.63 → 1.0.65
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/CHANGELOG.md +20 -0
- package/README.md +6 -4
- package/bin/agentlas.cjs +33 -5
- package/engine/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +130 -20
- package/engine/commands/browser.cjs +6 -5
- package/engine/commands/document.cjs +2 -2
- package/engine/telegram/connect.cjs +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.65 — 2026-08-29
|
|
4
|
+
|
|
5
|
+
- Browser, document, and Telegram CDP flows now create and track an Agentlas-owned
|
|
6
|
+
tab instead of attaching to the first page exposed by the debugging port. Existing
|
|
7
|
+
pages require explicit target selection, owned temporary pages are cleaned up, and
|
|
8
|
+
WebSocket attachment has a bounded open timeout.
|
|
9
|
+
- First-run SQLite bootstrap now starts at schema `user_version=106`, generated from
|
|
10
|
+
the Desktop migration ladder, including the multimodal model-role constraint.
|
|
11
|
+
|
|
12
|
+
## 1.0.64 — 2026-08-29
|
|
13
|
+
|
|
14
|
+
- First-run database bootstrap now refuses symbolic links and non-file entries
|
|
15
|
+
at `agentlas.sqlite` instead of following them for permission changes and
|
|
16
|
+
runtime access.
|
|
17
|
+
- A competing bootstrap is accepted only after its winning database path is a
|
|
18
|
+
real, non-empty file; undeletable empty placeholders now fail with the exact
|
|
19
|
+
cause instead of being reported as a successful concurrent initialization.
|
|
20
|
+
- The public runtime guidance now reflects that Kimi, Grok, and Cursor execute
|
|
21
|
+
through the shared ACP driver instead of describing their retired refusal.
|
|
22
|
+
|
|
3
23
|
## 1.0.63 — 2026-08-29
|
|
4
24
|
|
|
5
25
|
- `agentlas update --json` now reads the centralized output-mode context after
|
package/README.md
CHANGED
|
@@ -27,7 +27,9 @@ Agent Trust is our product principle: agent packages are treated as portable, ow
|
|
|
27
27
|
| **Agent CLI** | Requires at least one supported runtime CLI: `agy` (Antigravity, preferred), `claude`, `codex`, or legacy `gemini` in your `PATH`. Halts honestly with `no_runtime` if none are found (no fake model responses). |
|
|
28
28
|
| **OS** | macOS is verified by the current local release gate. Linux is covered by the public adapter/CI contract. A Windows launcher is provided, but this release does not claim independent end-to-end Windows verification. |
|
|
29
29
|
|
|
30
|
-
*Note:
|
|
30
|
+
*Note: active runtimes include `claude-code`, `codex`, `agy` (Antigravity),
|
|
31
|
+
legacy `gemini`, and the shared ACP-backed `kimi`, `grok`, and `cursor-agent`
|
|
32
|
+
adapters. `doctor` reports the exact locally executable set.*
|
|
31
33
|
|
|
32
34
|
## Installation
|
|
33
35
|
|
|
@@ -102,7 +104,7 @@ agentlas hep-local "<prompt>" # Same staffing, registered Local agents only
|
|
|
102
104
|
agentlas hep-cloud "<prompt>" # Same staffing, owner Agent Cloud only
|
|
103
105
|
agentlas hep-hub "<prompt>" # Same staffing, public Hub only
|
|
104
106
|
agentlas call "a,b" "<context>" # Call explicitly named Hub or Cloud agents
|
|
105
|
-
agentlas browser [...] # Real browser hardpoint launcher
|
|
107
|
+
agentlas browser [...] # Real browser hardpoint launcher (opens an owned tab)
|
|
106
108
|
agentlas route "<prompt>" [--json] # Preview routing decisions without execution
|
|
107
109
|
agentlas research <sub> # Research loadout (status|gather|search|read|plan)
|
|
108
110
|
```
|
|
@@ -228,7 +230,7 @@ The launcher (`bin/agentlas.cjs`) runs system Node against `engine/`. The defaul
|
|
|
228
230
|
| Windows | `%APPDATA%\Agentlas` |
|
|
229
231
|
| Linux | `$XDG_CONFIG_HOME/Agentlas` (default `~/.config/Agentlas`) |
|
|
230
232
|
|
|
231
|
-
The SQLite database file is `agentlas.sqlite` (`user_version=
|
|
233
|
+
The SQLite database file is `agentlas.sqlite` (`user_version=106`). When launched for the first time without an existing database, it bootstraps schemas using `engine/bootstrap-schema.sql`. Consequently, **projects, installed agents, task history, automation sessions, and MCP registrations are shared across Desktop and Terminal**. The first ordered project agent remains the controller; additional agents are task-scoped and are stored only as execution ledgers, never as global conversations or durable owners.
|
|
232
234
|
|
|
233
235
|
**Single migration authority.** The Desktop app owns the schema migration ladder; the CLI never migrates the shared database. `engine/bootstrap-schema.sql` is generated by running that ladder to completion against an empty database, so a CLI-created store already sits at the ladder head and has nothing left for Desktop to upgrade. If the CLI opens a store older than the version it knows, it refuses with an actionable message instead of migrating or silently proceeding — a second migrator on this lock-free file is what corrupted the store once before. On a machine with no Desktop app, an operator can set `AGENTLAS_STORE_MIGRATION_ROLE=owner` for a single deliberate upgrade run with every other Agentlas process closed.
|
|
234
236
|
|
|
@@ -278,7 +280,7 @@ agentlas doctor # Checks database, PATH runtimes, active CLI drivers, and c
|
|
|
278
280
|
agentlas --where # Outputs JSON diagnostic of launcher, engine, DB paths, driver, and Node version
|
|
279
281
|
```
|
|
280
282
|
|
|
281
|
-
- **`no_runtime: no agent CLI found`**: Connect Antigravity and put `agy` on `PATH` first, or install `claude`/`codex`; `gemini` remains available as a legacy CLI.
|
|
283
|
+
- **`no_runtime: no agent CLI found`**: Connect Antigravity and put `agy` on `PATH` first, or install `claude`/`codex`; `gemini` remains available as a legacy CLI. Kimi, Grok, and Cursor use the shared ACP driver when their local CLIs are available.
|
|
282
284
|
- **`runtime '<kind>' has no v2 streaming driver yet`**: Specified `--runtime` is not supported for active execution. Supported values: `claude-code`, `codex`, `agy` (Antigravity), `gemini` (legacy).
|
|
283
285
|
- **`Node vX — Node 22+ (node:sqlite) is required when better-sqlite3 is unavailable`**: `better-sqlite3` native build failed on Node 20/21. Upgrade to Node 22+ or install build tools for native compilation.
|
|
284
286
|
- **`storm`/`context`/`hep` halting due to missing runtime**: Agentlas OS core runtime is missing. Install Agentlas OS or set `HEPHAESTUS_BIN=<path>`.
|
package/bin/agentlas.cjs
CHANGED
|
@@ -68,6 +68,20 @@ function securePrivateMode(target, mode) {
|
|
|
68
68
|
fs.chmodSync(target, mode);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function databaseFileStat(target) {
|
|
72
|
+
let stat;
|
|
73
|
+
try {
|
|
74
|
+
stat = fs.lstatSync(target);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error && error.code === "ENOENT") return null;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
80
|
+
throw new Error(`Agentlas database path must be a regular file: ${target}`);
|
|
81
|
+
}
|
|
82
|
+
return stat;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
// ── SQLite 로더 (부트스트랩용): better-sqlite3 → node:sqlite ──
|
|
72
86
|
// require.resolve가 아니라 실제 로드로 판별한다 — ABI가 깨진 better-sqlite3나
|
|
73
87
|
// node:sqlite가 아직 없는/플래그가 필요한 Node(≤22.4 등)를 정확히 걸러낸다.
|
|
@@ -141,14 +155,17 @@ function bootstrapDbIfMissing() {
|
|
|
141
155
|
// 잡힌 파일이므로 없는 것으로 취급해 정상 부트스트랩 경로를 태운다. 내용이 있는데
|
|
142
156
|
// 손상된 경우는 여기서 판단하지 않는다 — 그건 복구지 부트스트랩이 아니고, 멀쩡한
|
|
143
157
|
// DB 를 빈 것으로 오판해 덮어쓰는 위험이 훨씬 크다.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!empty) {
|
|
158
|
+
const existing = databaseFileStat(p);
|
|
159
|
+
if (existing) {
|
|
160
|
+
if (existing.size > 0) {
|
|
148
161
|
securePrivateMode(p, 0o600);
|
|
149
162
|
return { created: false, path: p };
|
|
150
163
|
}
|
|
151
|
-
try {
|
|
164
|
+
try {
|
|
165
|
+
fs.rmSync(p);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(`Empty Agentlas database could not be replaced: ${error && error.message ? error.message : error}`);
|
|
168
|
+
}
|
|
152
169
|
}
|
|
153
170
|
const schemaFile = path.join(PKG_ROOT, "engine", "bootstrap-schema.sql");
|
|
154
171
|
if (!exists(schemaFile)) {
|
|
@@ -189,12 +206,23 @@ function bootstrapDbIfMissing() {
|
|
|
189
206
|
if (!e || e.code !== "EEXIST") {
|
|
190
207
|
throw new Error(`Atomic database bootstrap failed: ${e && e.message ? e.message : e}`);
|
|
191
208
|
}
|
|
209
|
+
// EEXIST only means another path entry won the name. It is a successful
|
|
210
|
+
// concurrent bootstrap only if that winner is already a real, non-empty
|
|
211
|
+
// database file; a symlink/directory/empty placeholder must fail closed.
|
|
212
|
+
const winner = databaseFileStat(p);
|
|
213
|
+
if (!winner || winner.size === 0) {
|
|
214
|
+
throw new Error(`Concurrent Agentlas database bootstrap produced an invalid file: ${p}`);
|
|
215
|
+
}
|
|
192
216
|
} finally {
|
|
193
217
|
try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
|
|
194
218
|
try { fs.rmSync(temp + "-journal", { force: true }); } catch { /* noop */ }
|
|
195
219
|
try { fs.rmSync(temp + "-wal", { force: true }); } catch { /* noop */ }
|
|
196
220
|
try { fs.rmSync(temp + "-shm", { force: true }); } catch { /* noop */ }
|
|
197
221
|
}
|
|
222
|
+
const finalStat = databaseFileStat(p);
|
|
223
|
+
if (!finalStat || finalStat.size === 0) {
|
|
224
|
+
throw new Error(`Agentlas database bootstrap did not produce a valid file: ${p}`);
|
|
225
|
+
}
|
|
198
226
|
securePrivateMode(p, 0o600);
|
|
199
227
|
return { created, path: p };
|
|
200
228
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
-- Agentlas 첫 실행 부트스트랩 스키마 (생성: 2026-08-
|
|
1
|
+
-- Agentlas 첫 실행 부트스트랩 스키마 (생성: 2026-08-29T13:29:10Z)
|
|
2
2
|
--
|
|
3
3
|
-- ★생성물이다. 손으로 고치지 말고 재생성하라:
|
|
4
4
|
-- node scripts/gen-bootstrap-schema.cjs
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
-- 정본은 Desktop 의 마이그레이션 사다리(agentlas_desktop/electron/store/db.ts, SCHEMA_VERSION).
|
|
7
7
|
-- 이 파일은 그 사다리를 **빈 DB** 에 끝까지 돌린 결과의 덤프이므로, 터미널이 만든 DB 는
|
|
8
8
|
-- 처음부터 사다리 머리에 있다 — 데스크탑이 나중에 승급할 것이 남지 않는다.
|
|
9
|
-
PRAGMA user_version=
|
|
9
|
+
PRAGMA user_version=106;
|
|
10
10
|
CREATE TABLE active_runtime (
|
|
11
11
|
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
12
12
|
kind TEXT NOT NULL
|
|
@@ -1015,7 +1015,7 @@ CREATE TABLE meta (
|
|
|
1015
1015
|
value TEXT NOT NULL
|
|
1016
1016
|
);
|
|
1017
1017
|
CREATE TABLE model_role_members (
|
|
1018
|
-
role TEXT NOT NULL CHECK(role IN ('orchestrator','worker')),
|
|
1018
|
+
role TEXT NOT NULL CHECK(role IN ('orchestrator','worker','multimodal')),
|
|
1019
1019
|
position INTEGER NOT NULL CHECK(position >= 1),
|
|
1020
1020
|
kind TEXT NOT NULL,
|
|
1021
1021
|
backend TEXT,
|
|
@@ -1027,7 +1027,7 @@ CREATE TABLE model_role_members (
|
|
|
1027
1027
|
PRIMARY KEY(role, position)
|
|
1028
1028
|
);
|
|
1029
1029
|
CREATE TABLE model_roles (
|
|
1030
|
-
role TEXT PRIMARY KEY CHECK(role IN ('orchestrator','worker')),
|
|
1030
|
+
role TEXT PRIMARY KEY CHECK(role IN ('orchestrator','worker','multimodal')),
|
|
1031
1031
|
kind TEXT NOT NULL,
|
|
1032
1032
|
backend TEXT,
|
|
1033
1033
|
source TEXT,
|
package/engine/browser/cdp.cjs
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
const http = require("node:http");
|
|
21
21
|
|
|
22
22
|
const DEFAULT_PORT = Number(process.env.AGENTLAS_CDP_PORT || 9222);
|
|
23
|
+
const DEFAULT_WS_OPEN_TIMEOUT_MS = 5_000;
|
|
24
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
23
25
|
|
|
24
26
|
function httpJson(port, path, { timeout = 3000, method = "GET" } = {}) {
|
|
25
27
|
return new Promise((resolve, reject) => {
|
|
@@ -46,16 +48,90 @@ async function cdpReady(port = DEFAULT_PORT) {
|
|
|
46
48
|
} catch { return false; }
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
function verifiedPageTarget(target, port, ownsTarget = false) {
|
|
52
|
+
if (!target || target.type !== "page" || typeof target.id !== "string" || typeof target.webSocketDebuggerUrl !== "string") return null;
|
|
53
|
+
try {
|
|
54
|
+
const socket = new URL(target.webSocketDebuggerUrl);
|
|
55
|
+
if (socket.protocol !== "ws:" || !LOOPBACK_HOSTS.has(socket.hostname.toLowerCase()) || Number(socket.port) !== port) return null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { id: target.id, webSocketDebuggerUrl: target.webSocketDebuggerUrl, ownsTarget };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function resolvePageTarget(port, { selection = "new", targetId = null } = {}) {
|
|
63
|
+
if (selection !== "new" && selection !== "existing") {
|
|
64
|
+
throw new Error(`unsupported CDP target selection: ${selection}`);
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
const rawTargets = await httpJson(port, "/json");
|
|
67
|
+
const pages = (Array.isArray(rawTargets) ? rawTargets : [])
|
|
68
|
+
.map((target) => verifiedPageTarget(target, port))
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
|
|
71
|
+
if (selection === "existing") {
|
|
72
|
+
if (!targetId) throw new Error("existing CDP target selection requires an explicit targetId");
|
|
73
|
+
const exact = pages.find((target) => target.id === String(targetId));
|
|
74
|
+
if (!exact) throw new Error(`no CDP page target matches targetId ${targetId}`);
|
|
75
|
+
return exact;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Never reuse an arbitrary first page. It may be a user's active tab, and
|
|
79
|
+
// browser/document/Telegram flows must not navigate it behind their back.
|
|
80
|
+
// Create a target we own, then return its socket identity to the caller.
|
|
81
|
+
const created = await httpJson(port, `/json/new?${encodeURIComponent("about:blank")}`, { method: "PUT" });
|
|
82
|
+
const exact = verifiedPageTarget(created, port, true);
|
|
83
|
+
if (!exact) throw new Error("CDP did not return a verified page target for the new tab");
|
|
84
|
+
return exact;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pick a page target without guessing ownership.
|
|
89
|
+
*
|
|
90
|
+
* The safe default is a fresh target. Reusing a page requires selection:
|
|
91
|
+
* `pickPageTarget(port, { selection: "existing", targetId })`.
|
|
92
|
+
*/
|
|
93
|
+
async function pickPageTarget(port = DEFAULT_PORT, options = {}) {
|
|
94
|
+
return (await resolvePageTarget(port, options)).webSocketDebuggerUrl;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function closePageTarget(port, targetId) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const req = http.request({
|
|
100
|
+
host: "127.0.0.1",
|
|
101
|
+
port,
|
|
102
|
+
path: `/json/close/${encodeURIComponent(String(targetId))}`,
|
|
103
|
+
timeout: 3_000,
|
|
104
|
+
}, (res) => {
|
|
105
|
+
res.resume();
|
|
106
|
+
res.once("end", resolve);
|
|
107
|
+
});
|
|
108
|
+
req.once("error", resolve);
|
|
109
|
+
req.once("timeout", () => { req.destroy(); resolve(); });
|
|
110
|
+
req.end();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function waitForWebSocketOpen(ws, timeoutMs) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
let settled = false;
|
|
117
|
+
let timer;
|
|
118
|
+
const finish = (error) => {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
if (error) reject(error);
|
|
123
|
+
else resolve();
|
|
124
|
+
};
|
|
125
|
+
timer = setTimeout(() => {
|
|
126
|
+
const error = new Error(`CDP websocket open timed out after ${timeoutMs}ms`);
|
|
127
|
+
error.code = "CDP_WEBSOCKET_OPEN_TIMEOUT";
|
|
128
|
+
try { ws.close(); } catch { /* already closed */ }
|
|
129
|
+
finish(error);
|
|
130
|
+
}, timeoutMs);
|
|
131
|
+
ws.addEventListener("open", () => finish(), { once: true });
|
|
132
|
+
ws.addEventListener("error", () => finish(new Error("CDP websocket failed to open")), { once: true });
|
|
133
|
+
ws.addEventListener("close", () => finish(new Error("CDP websocket closed before opening")), { once: true });
|
|
134
|
+
});
|
|
59
135
|
}
|
|
60
136
|
|
|
61
137
|
/**
|
|
@@ -65,16 +141,31 @@ async function pickPageTarget(port = DEFAULT_PORT) {
|
|
|
65
141
|
* evaluate(js, {awaitPromise}) Runtime.evaluate(returnByValue) — 값 반환
|
|
66
142
|
* waitFor(jsPredicate, {timeoutMs, pollMs}) predicate 가 truthy 될 때까지
|
|
67
143
|
*/
|
|
68
|
-
async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
144
|
+
async function attachPage({ port = DEFAULT_PORT, wsUrl, selection = "new", targetId, connectTimeoutMs = DEFAULT_WS_OPEN_TIMEOUT_MS } = {}) {
|
|
145
|
+
const target = wsUrl
|
|
146
|
+
? (() => {
|
|
147
|
+
try {
|
|
148
|
+
const socket = new URL(wsUrl);
|
|
149
|
+
if (socket.protocol !== "ws:" || !LOOPBACK_HOSTS.has(socket.hostname.toLowerCase()) || Number(socket.port) !== port) return null;
|
|
150
|
+
} catch { return null; }
|
|
151
|
+
return { id: null, webSocketDebuggerUrl: wsUrl, ownsTarget: false };
|
|
152
|
+
})()
|
|
153
|
+
: await resolvePageTarget(port, { selection, targetId });
|
|
154
|
+
if (!target) throw new Error(`invalid CDP websocket target for port ${port}`);
|
|
155
|
+
|
|
156
|
+
const url = target.webSocketDebuggerUrl;
|
|
157
|
+
let ws;
|
|
158
|
+
try {
|
|
159
|
+
ws = new WebSocket(url);
|
|
160
|
+
await waitForWebSocketOpen(ws, connectTimeoutMs);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (target.ownsTarget) await closePageTarget(port, target.id);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
75
165
|
|
|
76
166
|
let nextId = 0;
|
|
77
167
|
const pending = new Map();
|
|
168
|
+
let closed = false;
|
|
78
169
|
ws.addEventListener("message", (event) => {
|
|
79
170
|
let msg;
|
|
80
171
|
try { msg = JSON.parse(typeof event.data === "string" ? event.data : String(event.data)); } catch { return; }
|
|
@@ -85,8 +176,16 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
|
85
176
|
else resolve(msg.result);
|
|
86
177
|
}
|
|
87
178
|
});
|
|
179
|
+
ws.addEventListener("close", () => {
|
|
180
|
+
for (const { reject, timer } of pending.values()) {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
reject(new Error("CDP websocket closed"));
|
|
183
|
+
}
|
|
184
|
+
pending.clear();
|
|
185
|
+
}, { once: true });
|
|
88
186
|
|
|
89
187
|
function send(method, params = {}, { timeout = 30000 } = {}) {
|
|
188
|
+
if (closed) return Promise.reject(new Error("CDP page is closed"));
|
|
90
189
|
const id = ++nextId;
|
|
91
190
|
return new Promise((resolve, reject) => {
|
|
92
191
|
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP ${method} timed out`)); }, timeout);
|
|
@@ -94,7 +193,8 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
|
94
193
|
resolve: (r) => { clearTimeout(timer); resolve(r); },
|
|
95
194
|
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
96
195
|
});
|
|
97
|
-
ws.send(JSON.stringify({ id, method, params }));
|
|
196
|
+
try { ws.send(JSON.stringify({ id, method, params })); }
|
|
197
|
+
catch (error) { pending.delete(id); clearTimeout(timer); reject(error); }
|
|
98
198
|
});
|
|
99
199
|
}
|
|
100
200
|
|
|
@@ -186,9 +286,19 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
|
186
286
|
return { path: outPath, bytes: buf.length };
|
|
187
287
|
}
|
|
188
288
|
|
|
189
|
-
function close(
|
|
289
|
+
async function close({ closeTarget = false } = {}) {
|
|
290
|
+
if (closed) return;
|
|
291
|
+
closed = true;
|
|
292
|
+
try { ws.close(); } catch { /* already closed */ }
|
|
293
|
+
if (closeTarget && target.ownsTarget && target.id) await closePageTarget(port, target.id);
|
|
294
|
+
}
|
|
190
295
|
|
|
191
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
navigate, evaluate, evalExpr, waitFor, focusSelector, typeInto, pressKey,
|
|
298
|
+
clickSelector, printPdf, close, send,
|
|
299
|
+
targetId: target.id,
|
|
300
|
+
ownsTarget: target.ownsTarget,
|
|
301
|
+
};
|
|
192
302
|
}
|
|
193
303
|
|
|
194
|
-
module.exports = { cdpReady, pickPageTarget, attachPage, DEFAULT_PORT };
|
|
304
|
+
module.exports = { cdpReady, pickPageTarget, attachPage, DEFAULT_PORT, DEFAULT_WS_OPEN_TIMEOUT_MS };
|
|
@@ -93,12 +93,13 @@ async function browserLogin(ctx, rest) {
|
|
|
93
93
|
const url = siteUrl(row.site);
|
|
94
94
|
|
|
95
95
|
if (await cdp.cdpReady()) {
|
|
96
|
-
//
|
|
96
|
+
// 사용자의 현재 탭을 건드리지 않고 Agentlas 브라우저의 새 탭을 연다.
|
|
97
|
+
// 로그인은 사용자가 직접 한다.
|
|
97
98
|
try {
|
|
98
|
-
const page = await cdp.attachPage();
|
|
99
|
+
const page = await cdp.attachPage({ selection: "new" });
|
|
99
100
|
try { await page.navigate(url, { waitMs: 1500 }); } finally { page.close(); }
|
|
100
101
|
vault.logBrowserAction(db, { site: row.site, action: "login.open", target: url, result: "navigated" });
|
|
101
|
-
ctx.out(`${ctx.ui.green("✓")} ${ko ? "
|
|
102
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "새 브라우저 탭을 로그인 페이지로 열었습니다" : "opened a new browser tab for login"}: ${url}`);
|
|
102
103
|
} catch (e) {
|
|
103
104
|
ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
|
|
104
105
|
return 1;
|
|
@@ -140,10 +141,10 @@ async function browserGo(ctx, rest) {
|
|
|
140
141
|
return 1;
|
|
141
142
|
}
|
|
142
143
|
try {
|
|
143
|
-
const page = await cdp.attachPage();
|
|
144
|
+
const page = await cdp.attachPage({ selection: "new" });
|
|
144
145
|
let title;
|
|
145
146
|
try { await page.navigate(siteUrl(url), { waitMs: 1500 }); title = await page.evalExpr("document.title"); } finally { page.close(); }
|
|
146
|
-
ctx.out(`${ctx.ui.green("✓")} ${ko ? "
|
|
147
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "새 브라우저 탭에서 열었습니다" : "opened a new browser tab"}: ${siteUrl(url)}${title ? ctx.ui.dim(` — ${title}`) : ""}`);
|
|
147
148
|
return 0;
|
|
148
149
|
} catch (e) { ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`); return 1; }
|
|
149
150
|
}
|
|
@@ -51,7 +51,7 @@ async function toPdf(ctx, args) {
|
|
|
51
51
|
let page;
|
|
52
52
|
try {
|
|
53
53
|
if (typeof ctx.ui.startSpinner === "function") ctx.ui.startSpinner(ko ? "PDF 굽는 중…" : "Rendering PDF…");
|
|
54
|
-
page = await cdp.attachPage();
|
|
54
|
+
page = await cdp.attachPage({ selection: "new" });
|
|
55
55
|
await page.navigate(url, { waitMs: 1800 });
|
|
56
56
|
await page.waitFor("document.readyState === 'complete'", { timeoutMs: 8000 });
|
|
57
57
|
const r = await page.printPdf(outPath, { landscape: flags.landscape });
|
|
@@ -63,7 +63,7 @@ async function toPdf(ctx, args) {
|
|
|
63
63
|
ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
|
|
64
64
|
return 1;
|
|
65
65
|
} finally {
|
|
66
|
-
if (page) page.close();
|
|
66
|
+
if (page) await page.close({ closeTarget: true });
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -192,7 +192,7 @@ async function captureBotTokenViaBrowser({ timeoutMs = 180_000, onWait } = {}) {
|
|
|
192
192
|
err.code = "cdp_unavailable";
|
|
193
193
|
throw err;
|
|
194
194
|
}
|
|
195
|
-
const page = await cdp.attachPage();
|
|
195
|
+
const page = await cdp.attachPage({ selection: "new" });
|
|
196
196
|
try {
|
|
197
197
|
await page.navigate(BOTFATHER_WEB_URL, { waitMs: 2500 });
|
|
198
198
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -210,7 +210,7 @@ async function captureBotTokenViaBrowser({ timeoutMs = 180_000, onWait } = {}) {
|
|
|
210
210
|
}
|
|
211
211
|
return null;
|
|
212
212
|
} finally {
|
|
213
|
-
page.close();
|
|
213
|
+
await page.close({ closeTarget: true });
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.65",
|
|
4
4
|
"description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|