agentlas 1.0.64 → 1.0.66
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 +87 -0
- package/README.md +8 -6
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +315 -32
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +80 -28
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +308 -113
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +67 -34
- package/engine/commands/build.cjs +42 -12
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +40 -12
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +8 -2
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +29 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +230 -61
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +15 -5
- package/engine/mcp/plan.cjs +5 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +212 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +187 -24
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
package/engine/browser/cdp.cjs
CHANGED
|
@@ -17,22 +17,83 @@
|
|
|
17
17
|
* 안전: Agentlas 전용 프로필 Chrome 에만 붙는다(개인 크롬 아님). 되돌릴 수 없는
|
|
18
18
|
* 행동은 호출자가 판단한다 — 이 모듈은 조종 primitive 만 제공한다.
|
|
19
19
|
*/
|
|
20
|
+
const crypto = require("node:crypto");
|
|
21
|
+
const fs = require("node:fs");
|
|
20
22
|
const http = require("node:http");
|
|
23
|
+
const path = require("node:path");
|
|
21
24
|
|
|
22
25
|
const DEFAULT_PORT = Number(process.env.AGENTLAS_CDP_PORT || 9222);
|
|
26
|
+
const DEFAULT_WS_OPEN_TIMEOUT_MS = 5_000;
|
|
27
|
+
const CDP_HTTP_TIMEOUT_MS = 3_000;
|
|
28
|
+
const CDP_HTTP_MAX_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
const CDP_PDF_MAX_BYTES = 64 * 1024 * 1024;
|
|
30
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
23
31
|
|
|
24
|
-
function httpJson(port,
|
|
32
|
+
function httpJson(port, requestPath, { timeout = CDP_HTTP_TIMEOUT_MS, method = "GET" } = {}) {
|
|
33
|
+
const limit = Number.isFinite(Number(timeout)) && Number(timeout) > 0
|
|
34
|
+
? Number(timeout)
|
|
35
|
+
: CDP_HTTP_TIMEOUT_MS;
|
|
25
36
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
let req = null;
|
|
38
|
+
let response = null;
|
|
39
|
+
let timer = null;
|
|
40
|
+
let settled = false;
|
|
41
|
+
const finish = (error, value) => {
|
|
42
|
+
if (settled) return;
|
|
43
|
+
settled = true;
|
|
44
|
+
if (timer) clearTimeout(timer);
|
|
45
|
+
if (error) {
|
|
46
|
+
try { response?.destroy(); } catch { /* already closed */ }
|
|
47
|
+
try { req?.destroy(); } catch { /* already closed */ }
|
|
48
|
+
reject(error);
|
|
49
|
+
} else {
|
|
50
|
+
resolve(value);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const timeoutError = () => {
|
|
54
|
+
const error = new Error(`CDP ${requestPath} timed out`);
|
|
55
|
+
error.code = "CDP_HTTP_TIMEOUT";
|
|
56
|
+
return error;
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
req = http.request({ host: "127.0.0.1", port, path: requestPath, timeout: limit, method }, (res) => {
|
|
60
|
+
response = res;
|
|
61
|
+
const chunks = [];
|
|
62
|
+
let bodyBytes = 0;
|
|
63
|
+
res.on("data", (chunk) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
66
|
+
bodyBytes += bytes.length;
|
|
67
|
+
if (bodyBytes > CDP_HTTP_MAX_BYTES) {
|
|
68
|
+
const error = new Error(`CDP ${requestPath} response exceeded ${CDP_HTTP_MAX_BYTES} bytes`);
|
|
69
|
+
error.code = "CDP_HTTP_BODY_LIMIT";
|
|
70
|
+
finish(error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
chunks.push(bytes);
|
|
74
|
+
});
|
|
75
|
+
res.on("aborted", () => finish(new Error(`CDP ${requestPath} response aborted`)));
|
|
76
|
+
res.on("error", (error) => finish(error));
|
|
77
|
+
res.on("end", () => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
try {
|
|
80
|
+
finish(null, JSON.parse(Buffer.concat(chunks, bodyBytes).toString("utf8")));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
finish(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
31
85
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
req.on("error", (error) => finish(error));
|
|
87
|
+
// Keep the inactivity timeout as a fast-path, but also enforce the
|
|
88
|
+
// absolute deadline below so a peer cannot keep this request alive by
|
|
89
|
+
// dripping one byte before every inactivity interval.
|
|
90
|
+
req.on("timeout", () => finish(timeoutError()));
|
|
91
|
+
timer = setTimeout(() => finish(timeoutError()), limit);
|
|
92
|
+
timer.unref?.();
|
|
93
|
+
req.end();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
finish(error);
|
|
96
|
+
}
|
|
36
97
|
});
|
|
37
98
|
}
|
|
38
99
|
|
|
@@ -46,16 +107,184 @@ async function cdpReady(port = DEFAULT_PORT) {
|
|
|
46
107
|
} catch { return false; }
|
|
47
108
|
}
|
|
48
109
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
110
|
+
function verifiedPageTarget(target, port, ownsTarget = false) {
|
|
111
|
+
if (!target || target.type !== "page" || typeof target.id !== "string" || typeof target.webSocketDebuggerUrl !== "string") return null;
|
|
112
|
+
try {
|
|
113
|
+
const socket = new URL(target.webSocketDebuggerUrl);
|
|
114
|
+
if (socket.protocol !== "ws:" || !LOOPBACK_HOSTS.has(socket.hostname.toLowerCase()) || Number(socket.port) !== port) return null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
56
117
|
}
|
|
57
|
-
|
|
58
|
-
|
|
118
|
+
return { id: target.id, webSocketDebuggerUrl: target.webSocketDebuggerUrl, ownsTarget };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function resolvePageTarget(port, { selection = "new", targetId = null } = {}) {
|
|
122
|
+
if (selection !== "new" && selection !== "existing") {
|
|
123
|
+
throw new Error(`unsupported CDP target selection: ${selection}`);
|
|
124
|
+
}
|
|
125
|
+
const rawTargets = await httpJson(port, "/json");
|
|
126
|
+
const pages = (Array.isArray(rawTargets) ? rawTargets : [])
|
|
127
|
+
.map((target) => verifiedPageTarget(target, port))
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
|
|
130
|
+
if (selection === "existing") {
|
|
131
|
+
if (!targetId) throw new Error("existing CDP target selection requires an explicit targetId");
|
|
132
|
+
const exact = pages.find((target) => target.id === String(targetId));
|
|
133
|
+
if (!exact) throw new Error(`no CDP page target matches targetId ${targetId}`);
|
|
134
|
+
return exact;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Never reuse an arbitrary first page. It may be a user's active tab, and
|
|
138
|
+
// browser/document/Telegram flows must not navigate it behind their back.
|
|
139
|
+
// Create a target we own, then return its socket identity to the caller.
|
|
140
|
+
const created = await httpJson(port, `/json/new?${encodeURIComponent("about:blank")}`, { method: "PUT" });
|
|
141
|
+
const exact = verifiedPageTarget(created, port, true);
|
|
142
|
+
if (!exact) throw new Error("CDP did not return a verified page target for the new tab");
|
|
143
|
+
return exact;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Pick a page target without guessing ownership.
|
|
148
|
+
*
|
|
149
|
+
* The safe default is a fresh target. Reusing a page requires selection:
|
|
150
|
+
* `pickPageTarget(port, { selection: "existing", targetId })`.
|
|
151
|
+
*/
|
|
152
|
+
async function pickPageTarget(port = DEFAULT_PORT, options = {}) {
|
|
153
|
+
return (await resolvePageTarget(port, options)).webSocketDebuggerUrl;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function closePageTarget(port, targetId) {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
let settled = false;
|
|
159
|
+
let timer = null;
|
|
160
|
+
const finish = () => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
if (timer) clearTimeout(timer);
|
|
164
|
+
resolve();
|
|
165
|
+
};
|
|
166
|
+
const req = http.request({
|
|
167
|
+
host: "127.0.0.1",
|
|
168
|
+
port,
|
|
169
|
+
path: `/json/close/${encodeURIComponent(String(targetId))}`,
|
|
170
|
+
timeout: CDP_HTTP_TIMEOUT_MS,
|
|
171
|
+
}, (res) => {
|
|
172
|
+
res.resume();
|
|
173
|
+
res.once("end", finish);
|
|
174
|
+
res.once("aborted", finish);
|
|
175
|
+
res.once("error", finish);
|
|
176
|
+
});
|
|
177
|
+
req.once("error", finish);
|
|
178
|
+
req.once("timeout", () => { req.destroy(); finish(); });
|
|
179
|
+
timer = setTimeout(() => { req.destroy(); finish(); }, CDP_HTTP_TIMEOUT_MS);
|
|
180
|
+
timer.unref?.();
|
|
181
|
+
req.end();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sameFileIdentity(left, right) {
|
|
186
|
+
return left && right && String(left.dev) === String(right.dev) && String(left.ino) === String(right.ino);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function writeAtomicRegularFile(outPath, data) {
|
|
190
|
+
const target = path.resolve(String(outPath));
|
|
191
|
+
let original = null;
|
|
192
|
+
try {
|
|
193
|
+
original = fs.lstatSync(target);
|
|
194
|
+
if (!original.isFile() || original.nlink !== 1) {
|
|
195
|
+
const error = new Error(`CDP output path must be a regular file with one link: ${target}`);
|
|
196
|
+
error.code = "CDP_UNSAFE_OUTPUT_PATH";
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error && error.code !== "ENOENT") throw error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const dir = path.dirname(target);
|
|
204
|
+
const base = path.basename(target);
|
|
205
|
+
const flags = fs.constants.O_WRONLY
|
|
206
|
+
| fs.constants.O_CREAT
|
|
207
|
+
| fs.constants.O_EXCL
|
|
208
|
+
| (fs.constants.O_NOFOLLOW || 0);
|
|
209
|
+
let temp = null;
|
|
210
|
+
let fd = null;
|
|
211
|
+
try {
|
|
212
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
213
|
+
temp = path.join(dir, `.${base}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
214
|
+
try {
|
|
215
|
+
fd = fs.openSync(temp, flags, 0o600);
|
|
216
|
+
break;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
temp = null;
|
|
219
|
+
if (!error || error.code !== "EEXIST" || attempt === 2) throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (fd == null || !temp) throw new Error("could not create a temporary CDP output file");
|
|
223
|
+
let offset = 0;
|
|
224
|
+
while (offset < data.length) {
|
|
225
|
+
const written = fs.writeSync(fd, data, offset, data.length - offset);
|
|
226
|
+
if (!written) throw new Error("could not write the CDP output file");
|
|
227
|
+
offset += written;
|
|
228
|
+
}
|
|
229
|
+
fs.fsyncSync(fd);
|
|
230
|
+
fs.closeSync(fd);
|
|
231
|
+
fd = null;
|
|
232
|
+
|
|
233
|
+
// Refuse a path that changed identity while the temporary file was being
|
|
234
|
+
// written. The final rename itself never follows a symlink or hardlink.
|
|
235
|
+
let current = null;
|
|
236
|
+
try { current = fs.lstatSync(target); } catch (error) {
|
|
237
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
238
|
+
}
|
|
239
|
+
if (original) {
|
|
240
|
+
if (!current || !current.isFile() || current.nlink !== 1 || !sameFileIdentity(original, current)) {
|
|
241
|
+
const error = new Error(`CDP output path changed while writing: ${target}`);
|
|
242
|
+
error.code = "CDP_OUTPUT_IDENTITY_CHANGED";
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
} else if (current) {
|
|
246
|
+
// The caller observed an absent path. A file appearing before commit is
|
|
247
|
+
// a competing writer, even when it is otherwise a safe regular file.
|
|
248
|
+
// Never overwrite a file whose identity this operation did not capture.
|
|
249
|
+
const error = new Error(`CDP output path appeared while writing: ${target}`);
|
|
250
|
+
error.code = current.isFile() && current.nlink === 1
|
|
251
|
+
? "CDP_OUTPUT_IDENTITY_CHANGED"
|
|
252
|
+
: "CDP_UNSAFE_OUTPUT_PATH";
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
fs.renameSync(temp, target);
|
|
256
|
+
temp = null;
|
|
257
|
+
} finally {
|
|
258
|
+
if (fd != null) {
|
|
259
|
+
try { fs.closeSync(fd); } catch { /* already closed */ }
|
|
260
|
+
}
|
|
261
|
+
if (temp) {
|
|
262
|
+
try { fs.unlinkSync(temp); } catch { /* best-effort cleanup */ }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function waitForWebSocketOpen(ws, timeoutMs) {
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
let settled = false;
|
|
270
|
+
let timer;
|
|
271
|
+
const finish = (error) => {
|
|
272
|
+
if (settled) return;
|
|
273
|
+
settled = true;
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
if (error) reject(error);
|
|
276
|
+
else resolve();
|
|
277
|
+
};
|
|
278
|
+
timer = setTimeout(() => {
|
|
279
|
+
const error = new Error(`CDP websocket open timed out after ${timeoutMs}ms`);
|
|
280
|
+
error.code = "CDP_WEBSOCKET_OPEN_TIMEOUT";
|
|
281
|
+
try { ws.close(); } catch { /* already closed */ }
|
|
282
|
+
finish(error);
|
|
283
|
+
}, timeoutMs);
|
|
284
|
+
ws.addEventListener("open", () => finish(), { once: true });
|
|
285
|
+
ws.addEventListener("error", () => finish(new Error("CDP websocket failed to open")), { once: true });
|
|
286
|
+
ws.addEventListener("close", () => finish(new Error("CDP websocket closed before opening")), { once: true });
|
|
287
|
+
});
|
|
59
288
|
}
|
|
60
289
|
|
|
61
290
|
/**
|
|
@@ -65,16 +294,31 @@ async function pickPageTarget(port = DEFAULT_PORT) {
|
|
|
65
294
|
* evaluate(js, {awaitPromise}) Runtime.evaluate(returnByValue) — 값 반환
|
|
66
295
|
* waitFor(jsPredicate, {timeoutMs, pollMs}) predicate 가 truthy 될 때까지
|
|
67
296
|
*/
|
|
68
|
-
async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
297
|
+
async function attachPage({ port = DEFAULT_PORT, wsUrl, selection = "new", targetId, connectTimeoutMs = DEFAULT_WS_OPEN_TIMEOUT_MS } = {}) {
|
|
298
|
+
const target = wsUrl
|
|
299
|
+
? (() => {
|
|
300
|
+
try {
|
|
301
|
+
const socket = new URL(wsUrl);
|
|
302
|
+
if (socket.protocol !== "ws:" || !LOOPBACK_HOSTS.has(socket.hostname.toLowerCase()) || Number(socket.port) !== port) return null;
|
|
303
|
+
} catch { return null; }
|
|
304
|
+
return { id: null, webSocketDebuggerUrl: wsUrl, ownsTarget: false };
|
|
305
|
+
})()
|
|
306
|
+
: await resolvePageTarget(port, { selection, targetId });
|
|
307
|
+
if (!target) throw new Error(`invalid CDP websocket target for port ${port}`);
|
|
308
|
+
|
|
309
|
+
const url = target.webSocketDebuggerUrl;
|
|
310
|
+
let ws;
|
|
311
|
+
try {
|
|
312
|
+
ws = new WebSocket(url);
|
|
313
|
+
await waitForWebSocketOpen(ws, connectTimeoutMs);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (target.ownsTarget) await closePageTarget(port, target.id);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
75
318
|
|
|
76
319
|
let nextId = 0;
|
|
77
320
|
const pending = new Map();
|
|
321
|
+
let closed = false;
|
|
78
322
|
ws.addEventListener("message", (event) => {
|
|
79
323
|
let msg;
|
|
80
324
|
try { msg = JSON.parse(typeof event.data === "string" ? event.data : String(event.data)); } catch { return; }
|
|
@@ -85,8 +329,16 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
|
85
329
|
else resolve(msg.result);
|
|
86
330
|
}
|
|
87
331
|
});
|
|
332
|
+
ws.addEventListener("close", () => {
|
|
333
|
+
for (const { reject, timer } of pending.values()) {
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
reject(new Error("CDP websocket closed"));
|
|
336
|
+
}
|
|
337
|
+
pending.clear();
|
|
338
|
+
}, { once: true });
|
|
88
339
|
|
|
89
340
|
function send(method, params = {}, { timeout = 30000 } = {}) {
|
|
341
|
+
if (closed) return Promise.reject(new Error("CDP page is closed"));
|
|
90
342
|
const id = ++nextId;
|
|
91
343
|
return new Promise((resolve, reject) => {
|
|
92
344
|
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP ${method} timed out`)); }, timeout);
|
|
@@ -94,7 +346,8 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
|
94
346
|
resolve: (r) => { clearTimeout(timer); resolve(r); },
|
|
95
347
|
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
96
348
|
});
|
|
97
|
-
ws.send(JSON.stringify({ id, method, params }));
|
|
349
|
+
try { ws.send(JSON.stringify({ id, method, params })); }
|
|
350
|
+
catch (error) { pending.delete(id); clearTimeout(timer); reject(error); }
|
|
98
351
|
});
|
|
99
352
|
}
|
|
100
353
|
|
|
@@ -180,15 +433,45 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl } = {}) {
|
|
|
180
433
|
async function printPdf(outPath, { landscape = false, printBackground = true, scale = 1 } = {}) {
|
|
181
434
|
const res = await send("Page.printToPDF", { landscape, printBackground, scale, transferMode: "ReturnAsBase64" }, { timeout: 60000 });
|
|
182
435
|
const b64 = res && res.data;
|
|
183
|
-
if (!b64) throw new Error("Page.printToPDF returned no data");
|
|
436
|
+
if (typeof b64 !== "string" || !b64) throw new Error("Page.printToPDF returned no data");
|
|
437
|
+
if (b64.length > Math.ceil(CDP_PDF_MAX_BYTES / 3) * 4) {
|
|
438
|
+
const error = new Error(`Page.printToPDF response exceeded ${CDP_PDF_MAX_BYTES} bytes`);
|
|
439
|
+
error.code = "CDP_PDF_BODY_LIMIT";
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
184
442
|
const buf = Buffer.from(b64, "base64");
|
|
185
|
-
|
|
443
|
+
if (buf.length > CDP_PDF_MAX_BYTES) {
|
|
444
|
+
const error = new Error(`Page.printToPDF response exceeded ${CDP_PDF_MAX_BYTES} bytes`);
|
|
445
|
+
error.code = "CDP_PDF_BODY_LIMIT";
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
writeAtomicRegularFile(outPath, buf);
|
|
186
449
|
return { path: outPath, bytes: buf.length };
|
|
187
450
|
}
|
|
188
451
|
|
|
189
|
-
function close(
|
|
452
|
+
async function close({ closeTarget = false } = {}) {
|
|
453
|
+
if (closed) return;
|
|
454
|
+
closed = true;
|
|
455
|
+
try { ws.close(); } catch { /* already closed */ }
|
|
456
|
+
if (closeTarget && target.ownsTarget && target.id) await closePageTarget(port, target.id);
|
|
457
|
+
}
|
|
190
458
|
|
|
191
|
-
return {
|
|
459
|
+
return {
|
|
460
|
+
navigate, evaluate, evalExpr, waitFor, focusSelector, typeInto, pressKey,
|
|
461
|
+
clickSelector, printPdf, close, send,
|
|
462
|
+
targetId: target.id,
|
|
463
|
+
ownsTarget: target.ownsTarget,
|
|
464
|
+
};
|
|
192
465
|
}
|
|
193
466
|
|
|
194
|
-
module.exports = {
|
|
467
|
+
module.exports = {
|
|
468
|
+
cdpReady,
|
|
469
|
+
pickPageTarget,
|
|
470
|
+
attachPage,
|
|
471
|
+
DEFAULT_PORT,
|
|
472
|
+
DEFAULT_WS_OPEN_TIMEOUT_MS,
|
|
473
|
+
CDP_HTTP_TIMEOUT_MS,
|
|
474
|
+
CDP_HTTP_MAX_BYTES,
|
|
475
|
+
CDP_PDF_MAX_BYTES,
|
|
476
|
+
writeAtomicRegularFile,
|
|
477
|
+
};
|
package/engine/browser/vault.cjs
CHANGED
|
@@ -22,6 +22,7 @@ function normalizeSite(input) {
|
|
|
22
22
|
if (!raw) return "";
|
|
23
23
|
let u;
|
|
24
24
|
try { u = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`); } catch { return ""; }
|
|
25
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return "";
|
|
25
26
|
if (u.username || u.password) return ""; // 자격증명은 사이트 키에 절대 들이지 않는다
|
|
26
27
|
const host = u.host.toLowerCase().replace(/^www\./, "");
|
|
27
28
|
return host && !/\s/.test(host) ? host : "";
|
package/engine/cli-output.cjs
CHANGED
|
@@ -67,12 +67,23 @@ function textOf(value) {
|
|
|
67
67
|
return JSON.stringify(value);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function terminalTextOf(value, maxLength = Number.POSITIVE_INFINITY) {
|
|
71
|
+
const text = textOf(value);
|
|
72
|
+
if (typeof text !== "string") return "";
|
|
73
|
+
return text
|
|
74
|
+
.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "")
|
|
75
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
76
|
+
.replace(/\u001b[@-_]/g, "")
|
|
77
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ")
|
|
78
|
+
.slice(0, maxLength);
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
/**
|
|
71
82
|
* 표시 폭 — ANSI 를 벗기고, CJK 전각을 2칸으로 센다.
|
|
72
83
|
* 한글 표가 어긋나던 이유가 이것이다(폭을 문자 수로 세면 열이 밀린다).
|
|
73
84
|
*/
|
|
74
85
|
function displayWidth(value) {
|
|
75
|
-
const plain =
|
|
86
|
+
const plain = terminalTextOf(value);
|
|
76
87
|
let width = 0;
|
|
77
88
|
for (const char of plain) {
|
|
78
89
|
const code = char.codePointAt(0) ?? 0;
|
|
@@ -102,7 +113,7 @@ function padTo(value, width, align) {
|
|
|
102
113
|
function renderQuiet(result) {
|
|
103
114
|
const idField = result.schema.idField;
|
|
104
115
|
return rowsOf(result)
|
|
105
|
-
.map((item) =>
|
|
116
|
+
.map((item) => terminalTextOf(readField(item, idField)))
|
|
106
117
|
.filter((line) => line.length > 0)
|
|
107
118
|
.join("\n");
|
|
108
119
|
}
|
|
@@ -122,31 +133,65 @@ function renderYaml(result) {
|
|
|
122
133
|
if (value === null || value === undefined) return "null";
|
|
123
134
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
124
135
|
const text = textOf(value);
|
|
125
|
-
|
|
136
|
+
// YAML implicitly retypes unquoted strings such as yes/null/001/dates.
|
|
137
|
+
// Machine output must round-trip the command result without changing a
|
|
138
|
+
// string into a boolean, null, number, or timestamp. Conservatively leave
|
|
139
|
+
// only unambiguous word/path-like strings plain; JSON quoting is valid YAML.
|
|
140
|
+
const ambiguousWord = /^(?:~|null|true|false|yes|no|on|off|[-+]?\.?(?:inf|nan))$/i.test(text);
|
|
141
|
+
return /^[A-Za-z_./][\w./:@-]*$/.test(text) && !ambiguousWord ? text : JSON.stringify(text);
|
|
142
|
+
};
|
|
143
|
+
const isObject = (value) => value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
144
|
+
const assertBoundedTree = (root) => {
|
|
145
|
+
let nodes = 0;
|
|
146
|
+
const visit = (value, ancestors, depth) => {
|
|
147
|
+
if ((!isObject(value) && !Array.isArray(value)) || value instanceof Date) return;
|
|
148
|
+
if (depth > 64) throw new TypeError("YAML output exceeds the maximum nesting depth");
|
|
149
|
+
if (ancestors.has(value)) throw new TypeError("YAML output contains a circular reference");
|
|
150
|
+
nodes += 1;
|
|
151
|
+
if (nodes > 100_000) throw new TypeError("YAML output contains too many nodes");
|
|
152
|
+
const next = new Set(ancestors);
|
|
153
|
+
next.add(value);
|
|
154
|
+
for (const child of Array.isArray(value) ? value : Object.values(value)) visit(child, next, depth + 1);
|
|
155
|
+
};
|
|
156
|
+
visit(root, new Set(), 0);
|
|
126
157
|
};
|
|
127
158
|
const objectLines = (object, indent) => {
|
|
128
159
|
const pad = " ".repeat(indent);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (Array.isArray(value)) {
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
const entries = Object.entries(object);
|
|
161
|
+
if (!entries.length) return [`${pad}{}`];
|
|
162
|
+
return entries.flatMap(([key, value]) => {
|
|
163
|
+
const renderedKey = scalar(key);
|
|
164
|
+
if (isObject(value) || Array.isArray(value)) {
|
|
165
|
+
const nested = value.length === 0 ? [`${pad} ${Array.isArray(value) ? "[]" : "{}"}`] : valueLines(value, indent + 2);
|
|
166
|
+
if (nested.length === 1 && /^(?:\{\}|\[\])$/.test(nested[0].trim())) return [`${pad}${renderedKey}: ${nested[0].trim()}`];
|
|
167
|
+
return [`${pad}${renderedKey}:`, ...nested];
|
|
136
168
|
}
|
|
137
|
-
return `${pad}${
|
|
169
|
+
return [`${pad}${renderedKey}: ${scalar(value)}`];
|
|
138
170
|
});
|
|
139
171
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
const valueLines = (value, indent) => {
|
|
173
|
+
const pad = " ".repeat(indent);
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
if (!value.length) return [`${pad}[]`];
|
|
176
|
+
return value.flatMap((entry) => {
|
|
177
|
+
if (isObject(entry)) {
|
|
178
|
+
const nested = objectLines(entry, indent + 2);
|
|
179
|
+
return [`${pad}- ${nested[0].trimStart()}`, ...nested.slice(1)];
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(entry)) {
|
|
182
|
+
const nested = valueLines(entry, indent + 2);
|
|
183
|
+
return [`${pad}-`, ...nested];
|
|
184
|
+
}
|
|
185
|
+
return [`${pad}- ${scalar(entry)}`];
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (isObject(value)) return objectLines(value, indent);
|
|
189
|
+
return [`${pad}${scalar(value)}`];
|
|
190
|
+
};
|
|
191
|
+
const payload = result.type === "list" ? result.data.map(project) : project(result.data);
|
|
192
|
+
assertBoundedTree(payload);
|
|
193
|
+
if (result.type === "list" && result.data.length === 0) return "[]";
|
|
194
|
+
return valueLines(payload, 0).join("\n");
|
|
150
195
|
}
|
|
151
196
|
|
|
152
197
|
function renderTable(result, options) {
|
|
@@ -154,11 +199,11 @@ function renderTable(result, options) {
|
|
|
154
199
|
const rows = rowsOf(result);
|
|
155
200
|
if (rows.length === 0) return "";
|
|
156
201
|
const cells = rows.map((item) =>
|
|
157
|
-
columns.map((column) =>
|
|
202
|
+
columns.map((column) => terminalTextOf(readField(item, column.field))),
|
|
158
203
|
);
|
|
159
204
|
const widths = columns.map((column, index) =>
|
|
160
205
|
Math.max(
|
|
161
|
-
options.noHeaders ? 0 : displayWidth(column.header),
|
|
206
|
+
options.noHeaders ? 0 : displayWidth(terminalTextOf(column.header)),
|
|
162
207
|
...cells.map((row) => displayWidth(row[index] ?? "")),
|
|
163
208
|
),
|
|
164
209
|
);
|
|
@@ -166,7 +211,7 @@ function renderTable(result, options) {
|
|
|
166
211
|
if (!options.noHeaders) {
|
|
167
212
|
lines.push(
|
|
168
213
|
columns
|
|
169
|
-
.map((column, index) => padTo(column.header.toUpperCase(), widths[index], column.align))
|
|
214
|
+
.map((column, index) => padTo(terminalTextOf(column.header).toUpperCase(), widths[index], column.align))
|
|
170
215
|
.join(" ")
|
|
171
216
|
.trimEnd(),
|
|
172
217
|
);
|
|
@@ -212,12 +257,13 @@ function renderError(error, options = {}) {
|
|
|
212
257
|
const commandError = toCommandError(error);
|
|
213
258
|
if (opts.format === "json") return JSON.stringify({ error: commandError }, null, 2);
|
|
214
259
|
if (opts.format === "yaml") {
|
|
215
|
-
return ["error:", ` code: ${commandError.code}`, ` message: ${JSON.stringify(commandError.message)}`].join("\n");
|
|
260
|
+
return ["error:", ` code: ${JSON.stringify(commandError.code)}`, ` message: ${JSON.stringify(commandError.message)}`].join("\n");
|
|
216
261
|
}
|
|
217
262
|
const prefix = opts.noColor ? "Error: " : "[31mError: [0m";
|
|
263
|
+
const safeMessage = terminalTextOf(commandError.message, 8192);
|
|
218
264
|
return commandError.details && typeof commandError.details === "string" && opts.format === "table"
|
|
219
|
-
? `${prefix}${
|
|
220
|
-
: `${prefix}${
|
|
265
|
+
? `${prefix}${safeMessage}`
|
|
266
|
+
: `${prefix}${safeMessage}`;
|
|
221
267
|
}
|
|
222
268
|
|
|
223
269
|
/**
|
|
@@ -230,7 +276,12 @@ function renderError(error, options = {}) {
|
|
|
230
276
|
function parseOutputFlags(argv, env = process.env, isTty = Boolean(process.stdout.isTTY)) {
|
|
231
277
|
const rest = [];
|
|
232
278
|
const options = { ...DEFAULT_OPTIONS };
|
|
233
|
-
for (
|
|
279
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
280
|
+
const token = argv[index];
|
|
281
|
+
if (token === "--") {
|
|
282
|
+
rest.push(...argv.slice(index + 1));
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
234
285
|
if (token === "--json") options.format = "json";
|
|
235
286
|
else if (token === "--yaml") options.format = "yaml";
|
|
236
287
|
else if (token === "--quiet" || token === "-q") options.quiet = true;
|
|
@@ -259,4 +310,5 @@ module.exports = {
|
|
|
259
310
|
parseOutputFlags,
|
|
260
311
|
isRichUi,
|
|
261
312
|
displayWidth,
|
|
313
|
+
terminalTextOf,
|
|
262
314
|
};
|