@agentprojectcontext/apx 1.30.2 → 1.31.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/package.json +1 -1
- package/skills/apx-agency-agents/SKILL.md +1 -1
- package/skills/apx-agent/SKILL.md +6 -6
- package/skills/apx-project/SKILL.md +1 -2
- package/src/core/agent/prompt-builder.js +6 -0
- package/src/core/agent/run-agent.js +21 -0
- package/src/core/agent/self-memory.js +1 -1
- package/src/core/agent-memory.js +64 -0
- package/src/core/agent-system.js +3 -2
- package/src/core/scaffold.js +43 -18
- package/src/core/tools/browser.js +169 -75
- package/src/core/tools/registry.js +13 -8
- package/src/core/tools/search.js +35 -7
- package/src/host/daemon/api/agents.js +19 -21
- package/src/host/daemon/api/sessions-search.js +1 -1
- package/src/host/daemon/api/shared.js +5 -8
- package/src/host/daemon/super-agent-tools/index.js +232 -43
- package/src/host/daemon/super-agent-tools/registry-bridge.js +30 -1
- package/src/host/daemon/super-agent-tools/tools/discover-tools.js +67 -0
- package/src/host/daemon/super-agent-tools/tools/import-agent.js +2 -0
- package/src/host/daemon/super-agent-tools/tools/read-agent-memory.js +5 -4
- package/src/host/daemon/super-agent.js +15 -17
- package/src/interfaces/cli/commands/agent.js +4 -1
- package/src/interfaces/cli/commands/memory.js +9 -10
- package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js → index-BV615I9p.js} +5 -5
- package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js.map → index-BV615I9p.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/package-lock.json +100 -211
- package/src/interfaces/web/src/i18n/en.ts +6 -6
- package/src/interfaces/web/src/i18n/es.ts +6 -6
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
package/package.json
CHANGED
|
@@ -59,7 +59,7 @@ apx agent vault list --all
|
|
|
59
59
|
# Create a new template (writes ~/.apx/agents/<slug>.md)
|
|
60
60
|
apx agent vault add reviewer \
|
|
61
61
|
--role "Code reviewer" \
|
|
62
|
-
--model
|
|
62
|
+
--model ollama:llama3.2:3b \
|
|
63
63
|
--language es \
|
|
64
64
|
--skills code-review,git \
|
|
65
65
|
--description "Reviews PRs and pushes back on hand-wavy diffs."
|
|
@@ -5,7 +5,7 @@ description: How to create, configure, and use project agents in APX. Load when
|
|
|
5
5
|
|
|
6
6
|
# apx-agent
|
|
7
7
|
|
|
8
|
-
A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data
|
|
8
|
+
A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data (memory, conversations, sessions) lives under `~/.apx/projects/<apx_id>/agents/<slug>/` and is never committed. APX still reads legacy `.apc/agents/<slug>/memory.md` as a migration fallback only.
|
|
9
9
|
|
|
10
10
|
## Concrete CLI calls
|
|
11
11
|
|
|
@@ -13,10 +13,10 @@ A project agent is a named persona inside an APC project. Canonical definition:
|
|
|
13
13
|
# List agents in a project
|
|
14
14
|
apx agent list --project iacrmar
|
|
15
15
|
|
|
16
|
-
# Create a new agent (writes .apc/agents/<slug>.md
|
|
16
|
+
# Create a new agent (writes .apc/agents/<slug>.md, creates runtime dir, regenerates AGENTS.md)
|
|
17
17
|
apx agent add reviewer \
|
|
18
18
|
--role "Code reviewer" \
|
|
19
|
-
--model
|
|
19
|
+
--model ollama:llama3.2:3b \
|
|
20
20
|
--language es \
|
|
21
21
|
--description "Reviews PRs and pushes back on hand-wavy diffs." \
|
|
22
22
|
--tools read,write,run \
|
|
@@ -45,7 +45,7 @@ apx memory <slug> --project iacrmar --replace < file.md # full replace fro
|
|
|
45
45
|
2. Description (from AGENTS.md).
|
|
46
46
|
3. Role + Language fields.
|
|
47
47
|
4. Invocation context: `engine | telegram | routine | runtime` — the channel calling the agent.
|
|
48
|
-
5. Memory:
|
|
48
|
+
5. Memory: `~/.apx/projects/<apx_id>/agents/<slug>/memory.md` if it exists, with legacy `.apc/agents/<slug>/memory.md` as a migration fallback.
|
|
49
49
|
6. Skills declared in the agent's `Skills:` field, each loaded from `.apc/skills/<slug>.md` or the bundled set.
|
|
50
50
|
7. The `apx` meta-skill (so the agent knows how to operate APX).
|
|
51
51
|
8. ACTION_DISCIPLINE_RULES (fixed footer — anti-ghost, anti-disclaimer, action-first).
|
|
@@ -54,13 +54,13 @@ That's the prompt the engine sees on every `apx exec <agent>` or `apx chat <agen
|
|
|
54
54
|
|
|
55
55
|
## Models per agent
|
|
56
56
|
|
|
57
|
-
Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model.
|
|
57
|
+
Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model. Leave it empty when the agent should follow the project/global default.
|
|
58
58
|
|
|
59
59
|
```markdown
|
|
60
60
|
# .apc/agents/reviewer.md
|
|
61
61
|
---
|
|
62
62
|
Role: Code reviewer
|
|
63
|
-
Model:
|
|
63
|
+
Model: ollama:llama3.2:3b ← this agent uses this model, independent of super_agent.model
|
|
64
64
|
Language: es
|
|
65
65
|
---
|
|
66
66
|
```
|
|
@@ -57,7 +57,6 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
|
|
|
57
57
|
└── .apc/
|
|
58
58
|
├── project.json ← { apxId, name, ... }
|
|
59
59
|
├── agents/<slug>.md
|
|
60
|
-
├── agents/<slug>/memory.md
|
|
61
60
|
├── skills/<slug>.md or <slug>/SKILL.md
|
|
62
61
|
├── mcps.json ← shared MCPs (committed)
|
|
63
62
|
├── commands/ ← custom slash-commands
|
|
@@ -65,7 +64,7 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
|
|
|
65
64
|
|
|
66
65
|
~/.apx/projects/<apxId>/ ← runtime state (never committed)
|
|
67
66
|
├── messages/YYYY-MM-DD.jsonl
|
|
68
|
-
├── agents/<slug>/{sessions/, conversations/}
|
|
67
|
+
├── agents/<slug>/{memory.md, sessions/, conversations/}
|
|
69
68
|
├── routines.json
|
|
70
69
|
├── tasks/YYYY-MM.jsonl
|
|
71
70
|
├── artifacts/
|
|
@@ -271,6 +271,11 @@ export function buildSuperAgentSystem({
|
|
|
271
271
|
// Pre-rendered "# Hilos activos en otros canales" block (recency-based
|
|
272
272
|
// cross-channel awareness; see core/memory/active-threads.js). "" → omitted.
|
|
273
273
|
activeThreadsBlock = "",
|
|
274
|
+
// Compact "# Tools adicionales (activación on-demand)" block: instructions +
|
|
275
|
+
// the NAMES (no schemas) of tools that exist but aren't loaded on this
|
|
276
|
+
// channel, so the model knows they're reachable via discover_tools without
|
|
277
|
+
// paying for their schemas. "" → omitted (full channels load everything).
|
|
278
|
+
lazyToolsBlock = "",
|
|
274
279
|
}) {
|
|
275
280
|
const sa = globalConfig.super_agent;
|
|
276
281
|
const projectIndex = projects
|
|
@@ -305,6 +310,7 @@ export function buildSuperAgentSystem({
|
|
|
305
310
|
projectIndex || "(no projects registered)",
|
|
306
311
|
buildProjectAgentsBlock(channelMeta?.projectPath),
|
|
307
312
|
buildSkillsCatalog(listSkills),
|
|
313
|
+
lazyToolsBlock,
|
|
308
314
|
voiceModeBlock,
|
|
309
315
|
systemSuffix,
|
|
310
316
|
]
|
|
@@ -178,6 +178,25 @@ export async function runAgent({
|
|
|
178
178
|
})
|
|
179
179
|
: rawHandlers;
|
|
180
180
|
|
|
181
|
+
// Lazy tools: when the super-agent runs a `discover_tools` activation, its
|
|
182
|
+
// handler pushes the newly-revealed schemas onto session.pending. We drain
|
|
183
|
+
// that queue into effectiveSchemas at the top of each iteration, so tools
|
|
184
|
+
// activated on step N are callable from step N+1. No session → no-op.
|
|
185
|
+
const toolSession = toolHandlerCtx?.toolSession || null;
|
|
186
|
+
const drainPendingTools = () => {
|
|
187
|
+
if (!toolSession || toolSession.pending.length === 0) return;
|
|
188
|
+
const seen = new Set(
|
|
189
|
+
effectiveSchemas.map((s) => s?.function?.name || s?.name)
|
|
190
|
+
);
|
|
191
|
+
const additions = [];
|
|
192
|
+
for (const sc of toolSession.pending) {
|
|
193
|
+
const n = sc?.function?.name || sc?.name;
|
|
194
|
+
if (n && !seen.has(n)) { additions.push(sc); seen.add(n); }
|
|
195
|
+
}
|
|
196
|
+
toolSession.pending = [];
|
|
197
|
+
if (additions.length > 0) effectiveSchemas = effectiveSchemas.concat(additions);
|
|
198
|
+
};
|
|
199
|
+
|
|
181
200
|
const conversation = [...previousMessages, { role: "user", content: prompt }];
|
|
182
201
|
const trace = [];
|
|
183
202
|
let totalUsage = { input_tokens: 0, output_tokens: 0 };
|
|
@@ -236,6 +255,8 @@ export async function runAgent({
|
|
|
236
255
|
};
|
|
237
256
|
|
|
238
257
|
for (let iter = 0; iter < maxIters; iter++) {
|
|
258
|
+
// Merge any tools activated via discover_tools on the previous iteration.
|
|
259
|
+
drainPendingTools();
|
|
239
260
|
await emitProgress(onEvent, { type: "model_start", iteration: iter + 1, model: activeModel });
|
|
240
261
|
// Force a tool call on iter 0 ONLY when the user message looks like a real
|
|
241
262
|
// action request ("listame…", "mandá…", "buscá…"). For chit-chat ("hola",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This is distinct from:
|
|
4
4
|
// - identity.json → who Roby is (name, personality, owner)
|
|
5
|
-
// - project agents'
|
|
5
|
+
// - project agents' ~/.apx/projects/<apx_id>/agents/<slug>/memory.md → per-agent, per-project
|
|
6
6
|
// - sessions → raw transcripts of past work (search_sessions)
|
|
7
7
|
//
|
|
8
8
|
// It is a single free-form markdown file at ~/.apx/memory.md that Roby keeps
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { projectStorageRoot } from "./config.js";
|
|
4
|
+
import { getOrCreateApxId } from "./scaffold.js";
|
|
5
|
+
|
|
6
|
+
const EMPTY_MEMORY = (slug) =>
|
|
7
|
+
`# Memory — ${slug}\n\n` +
|
|
8
|
+
`## Identity\n- \n\n` +
|
|
9
|
+
`## Long-term facts\n- \n\n` +
|
|
10
|
+
`## Recent context\n- \n`;
|
|
11
|
+
|
|
12
|
+
export function agentRuntimeDir(projectOrRoot, slug) {
|
|
13
|
+
const storagePath =
|
|
14
|
+
typeof projectOrRoot === "object" && projectOrRoot?.storagePath
|
|
15
|
+
? projectOrRoot.storagePath
|
|
16
|
+
: null;
|
|
17
|
+
const root =
|
|
18
|
+
typeof projectOrRoot === "string"
|
|
19
|
+
? projectOrRoot
|
|
20
|
+
: projectOrRoot?.path;
|
|
21
|
+
const base = storagePath || projectStorageRoot(getOrCreateApxId(root));
|
|
22
|
+
return path.join(base, "agents", slug);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function agentMemoryPath(projectOrRoot, slug) {
|
|
26
|
+
return path.join(agentRuntimeDir(projectOrRoot, slug), "memory.md");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function legacyAgentMemoryPath(projectRoot, slug) {
|
|
30
|
+
return path.join(projectRoot, ".apc", "agents", slug, "memory.md");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ensureAgentRuntimeDir(projectOrRoot, slug, { createMemory = false } = {}) {
|
|
34
|
+
const dir = agentRuntimeDir(projectOrRoot, slug);
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
if (createMemory) {
|
|
37
|
+
const memory = path.join(dir, "memory.md");
|
|
38
|
+
if (!fs.existsSync(memory)) fs.writeFileSync(memory, EMPTY_MEMORY(slug));
|
|
39
|
+
}
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readAgentMemory(projectOrRoot, slug) {
|
|
44
|
+
const primary = agentMemoryPath(projectOrRoot, slug);
|
|
45
|
+
if (fs.existsSync(primary)) return fs.readFileSync(primary, "utf8");
|
|
46
|
+
|
|
47
|
+
const root =
|
|
48
|
+
typeof projectOrRoot === "string"
|
|
49
|
+
? projectOrRoot
|
|
50
|
+
: projectOrRoot?.path;
|
|
51
|
+
if (root) {
|
|
52
|
+
const legacy = legacyAgentMemoryPath(root, slug);
|
|
53
|
+
if (fs.existsSync(legacy)) return fs.readFileSync(legacy, "utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function writeAgentMemory(projectOrRoot, slug, body) {
|
|
60
|
+
ensureAgentRuntimeDir(projectOrRoot, slug);
|
|
61
|
+
const memory = agentMemoryPath(projectOrRoot, slug);
|
|
62
|
+
fs.writeFileSync(memory, body);
|
|
63
|
+
return memory;
|
|
64
|
+
}
|
package/src/core/agent-system.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { readAgentMemory } from "./agent-memory.js";
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Anti-ghost-response rules injected into every agent system prompt.
|
|
@@ -62,8 +63,8 @@ export function buildAgentSystem(project, agent, {
|
|
|
62
63
|
|
|
63
64
|
parts.push(buildInvocationContext({ invocation, runtime, channel, caller, routine }));
|
|
64
65
|
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
66
|
+
const memory = readAgentMemory(project, agent.slug);
|
|
67
|
+
if (memory) parts.push("## Memory\n" + memory);
|
|
67
68
|
|
|
68
69
|
const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
|
|
69
70
|
if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
|
package/src/core/scaffold.js
CHANGED
|
@@ -325,24 +325,60 @@ const AGENTS_MD_TEMPLATE = `# AGENTS.md
|
|
|
325
325
|
<!-- Hard constraints: what agents must always / never do here. -->
|
|
326
326
|
`;
|
|
327
327
|
|
|
328
|
-
const APC_GITIGNORE = `# APC
|
|
329
|
-
#
|
|
330
|
-
|
|
331
|
-
agents
|
|
328
|
+
const APC_GITIGNORE = `# APC repository-safe context only.
|
|
329
|
+
# Runtime state belongs in ~/.apx/projects/<id>/, not in .apc/.
|
|
330
|
+
|
|
331
|
+
# Legacy per-agent runtime dirs (agent definitions are flat: agents/<slug>.md)
|
|
332
|
+
agents/*/
|
|
333
|
+
|
|
334
|
+
# Runtime sessions / conversations / messages
|
|
332
335
|
sessions/
|
|
333
336
|
conversations/
|
|
334
337
|
messages/
|
|
335
338
|
chats/
|
|
339
|
+
threads/
|
|
340
|
+
transcripts/
|
|
341
|
+
runs/
|
|
342
|
+
|
|
343
|
+
# Runtime memory, indexes, databases, and caches
|
|
344
|
+
memory.local.md
|
|
345
|
+
auto-memory.md
|
|
336
346
|
cache/
|
|
337
347
|
tmp/
|
|
338
348
|
private/
|
|
339
349
|
secrets/
|
|
350
|
+
*.db
|
|
351
|
+
*.db-*
|
|
352
|
+
*.sqlite
|
|
353
|
+
*.sqlite3
|
|
354
|
+
project.db
|
|
355
|
+
memory.db
|
|
356
|
+
memory-index.jsonl
|
|
357
|
+
memory-cursor.json
|
|
358
|
+
|
|
359
|
+
# Local config and secrets
|
|
360
|
+
.env
|
|
340
361
|
*.local.json
|
|
341
362
|
*.secret.json
|
|
342
363
|
*.env
|
|
343
364
|
*.env.*
|
|
344
|
-
|
|
365
|
+
*.key
|
|
366
|
+
*.pem
|
|
367
|
+
*.p12
|
|
368
|
+
*.crt
|
|
369
|
+
credentials.json
|
|
370
|
+
service-account*.json
|
|
371
|
+
token*.json
|
|
372
|
+
mcps.local.json
|
|
373
|
+
config.local.json
|
|
374
|
+
|
|
375
|
+
# Scratch planning state
|
|
376
|
+
plans/scratch/
|
|
377
|
+
plans/*.local.md
|
|
378
|
+
|
|
379
|
+
# Migration marker and OS noise
|
|
345
380
|
migrate.md
|
|
381
|
+
.DS_Store
|
|
346
382
|
`;
|
|
347
383
|
|
|
348
384
|
function nowIso() {
|
|
@@ -448,19 +484,8 @@ export function initApf(directory, { name } = {}) {
|
|
|
448
484
|
}
|
|
449
485
|
|
|
450
486
|
export function ensureAgentDir(root, slug) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const memory = path.join(dir, "memory.md");
|
|
454
|
-
if (!fs.existsSync(memory)) {
|
|
455
|
-
fs.writeFileSync(
|
|
456
|
-
memory,
|
|
457
|
-
`# Memory — ${slug}\n\n` +
|
|
458
|
-
`## Identity\n- \n\n` +
|
|
459
|
-
`## Long-term facts\n- \n\n` +
|
|
460
|
-
`## Recent context\n- \n`
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
return dir;
|
|
487
|
+
fs.mkdirSync(path.join(root, ".apc", "agents"), { recursive: true });
|
|
488
|
+
return path.join(root, ".apc", "agents");
|
|
464
489
|
}
|
|
465
490
|
|
|
466
491
|
// Write .apc/agents/<slug>.md — the canonical agent definition file.
|
|
@@ -181,19 +181,104 @@ async function ensureBrowser({ launch_options, allow_dangerous } = {}) {
|
|
|
181
181
|
return _page;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Context-destruction resilience
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
189
|
+
|
|
190
|
+
// Puppeteer throws this family of errors when an action (evaluate / get_text /
|
|
191
|
+
// click / …) runs while the page is navigating, redirecting, or reloading —
|
|
192
|
+
// the frame's JS execution context is torn down mid-call. Redirect-heavy sites
|
|
193
|
+
// (ESPN geo/consent hops, login walls) trigger it constantly. These are
|
|
194
|
+
// transient: waiting for the navigation to settle and retrying succeeds.
|
|
195
|
+
const CONTEXT_DESTROYED_RE =
|
|
196
|
+
/Execution context was destroyed|Cannot find context|Execution context is not available|detached frame|frame (?:was|got) detached|Target closed|Session closed|Protocol error.*(?:Runtime|Page)\./i;
|
|
197
|
+
|
|
198
|
+
export function isContextDestroyed(err) {
|
|
199
|
+
return CONTEXT_DESTROYED_RE.test(String(err?.message || err));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Let any in-flight navigation finish so the next action sees a stable context.
|
|
203
|
+
async function settlePage(page, { timeout = 5000 } = {}) {
|
|
204
|
+
if (!page || page.isClosed()) return;
|
|
205
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout }).catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Run a page action, retrying on a transient "Execution context was destroyed"
|
|
209
|
+
// (and friends): wait `delayMs`, let the page settle, try again — up to
|
|
210
|
+
// `retries` extra attempts. Non-context errors bubble immediately.
|
|
211
|
+
export async function withContextRetry(fn, { retries = 2, delayMs = 1500 } = {}) {
|
|
212
|
+
let lastErr;
|
|
213
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
214
|
+
try {
|
|
215
|
+
return await fn();
|
|
216
|
+
} catch (e) {
|
|
217
|
+
lastErr = e;
|
|
218
|
+
if (!isContextDestroyed(e) || attempt === retries) throw e;
|
|
219
|
+
await sleep(delayMs);
|
|
220
|
+
await settlePage(_page);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw lastErr;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Convenience: ensure the browser/page, then run an action under context-retry.
|
|
227
|
+
async function onPage(fn) {
|
|
228
|
+
const page = await ensureBrowser();
|
|
229
|
+
return withContextRetry(() => fn(page));
|
|
230
|
+
}
|
|
231
|
+
|
|
184
232
|
// ---------------------------------------------------------------------------
|
|
185
233
|
// Tool implementations
|
|
186
234
|
// ---------------------------------------------------------------------------
|
|
187
235
|
|
|
188
|
-
export async function browser_navigate({ url, launch_options, allow_dangerous } = {}) {
|
|
236
|
+
export async function browser_navigate({ url, launch_options, allow_dangerous, wait_until } = {}) {
|
|
189
237
|
if (!url) throw new Error("url required");
|
|
190
238
|
const page = await ensureBrowser({ launch_options, allow_dangerous });
|
|
191
|
-
|
|
239
|
+
|
|
240
|
+
const go = async (waitUntil) => {
|
|
241
|
+
const response = await page.goto(url, { waitUntil, timeout: 30000 });
|
|
242
|
+
// Some sites fire a client-side redirect/reload right after the initial
|
|
243
|
+
// load. Give it a beat to settle so the execution context is stable for
|
|
244
|
+
// the caller's NEXT tool (get_text/evaluate) instead of being destroyed
|
|
245
|
+
// out from under it.
|
|
246
|
+
await settlePage(page, { timeout: 3000 });
|
|
247
|
+
return response;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Preferred wait strategy: networkidle2 (or caller override). On a
|
|
251
|
+
// context-destroyed / timeout / navigation error, fall back to the much more
|
|
252
|
+
// permissive "domcontentloaded" which resolves as soon as the DOM is parsed,
|
|
253
|
+
// before late redirects/XHR can tear the context down.
|
|
254
|
+
const preferred = wait_until || "networkidle2";
|
|
255
|
+
let response;
|
|
256
|
+
try {
|
|
257
|
+
response = await go(preferred);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
const recoverable =
|
|
260
|
+
isContextDestroyed(e) ||
|
|
261
|
+
/TimeoutError|Navigation timeout|net::ERR_ABORTED|frame was detached/i.test(String(e?.message || e));
|
|
262
|
+
if (!recoverable || preferred === "domcontentloaded") throw e;
|
|
263
|
+
await sleep(1500);
|
|
264
|
+
response = await go("domcontentloaded");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// title() evaluates in-page, so it can itself throw if a redirect is still
|
|
268
|
+
// in flight — read it defensively (url() is sync and always safe).
|
|
269
|
+
let title = "";
|
|
270
|
+
try {
|
|
271
|
+
title = await withContextRetry(() => page.title(), { retries: 1, delayMs: 1000 });
|
|
272
|
+
} catch {
|
|
273
|
+
title = "";
|
|
274
|
+
}
|
|
275
|
+
|
|
192
276
|
return {
|
|
193
277
|
ok: true,
|
|
194
278
|
url: page.url(),
|
|
195
279
|
status: response?.status() ?? null,
|
|
196
|
-
title
|
|
280
|
+
title,
|
|
281
|
+
wait_until: response ? (preferred) : null,
|
|
197
282
|
};
|
|
198
283
|
}
|
|
199
284
|
|
|
@@ -206,12 +291,13 @@ export async function browser_screenshot({ selector, full_page = false, width, h
|
|
|
206
291
|
});
|
|
207
292
|
}
|
|
208
293
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
294
|
+
const buf = await withContextRetry(async () => {
|
|
295
|
+
const target = selector ? await page.$(selector) : null;
|
|
296
|
+
if (selector && !target) throw new Error(`Element not found: ${selector}`);
|
|
297
|
+
return target
|
|
298
|
+
? await target.screenshot({ type: "png", encoding: "base64" })
|
|
299
|
+
: await page.screenshot({ type: "png", encoding: "base64", fullPage: !!full_page });
|
|
300
|
+
});
|
|
215
301
|
|
|
216
302
|
const size = Buffer.from(String(buf), "base64").length;
|
|
217
303
|
if (size > MAX_SCREENSHOT_BYTES) {
|
|
@@ -248,50 +334,56 @@ export async function browser_screenshot({ selector, full_page = false, width, h
|
|
|
248
334
|
|
|
249
335
|
export async function browser_click({ selector } = {}) {
|
|
250
336
|
if (!selector) throw new Error("selector required");
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
337
|
+
return onPage(async (page) => {
|
|
338
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
339
|
+
await page.click(selector);
|
|
340
|
+
await page.waitForNetworkIdle({ timeout: 5000 }).catch(() => {});
|
|
341
|
+
return { ok: true, selector, url: page.url() };
|
|
342
|
+
});
|
|
256
343
|
}
|
|
257
344
|
|
|
258
345
|
export async function browser_type({ selector, text, clear = true } = {}) {
|
|
259
346
|
if (!selector) throw new Error("selector required");
|
|
260
347
|
if (text === undefined) throw new Error("text required");
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
348
|
+
return onPage(async (page) => {
|
|
349
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
350
|
+
await page.focus(selector);
|
|
351
|
+
if (clear) {
|
|
352
|
+
await page.keyboard.down("Control");
|
|
353
|
+
await page.keyboard.press("KeyA");
|
|
354
|
+
await page.keyboard.up("Control");
|
|
355
|
+
await page.keyboard.press("Backspace");
|
|
356
|
+
}
|
|
357
|
+
await page.type(selector, String(text), { delay: 20 });
|
|
358
|
+
return { ok: true, selector, typed: String(text).length };
|
|
359
|
+
});
|
|
272
360
|
}
|
|
273
361
|
|
|
274
362
|
export async function browser_select({ selector, value } = {}) {
|
|
275
363
|
if (!selector) throw new Error("selector required");
|
|
276
364
|
if (value === undefined) throw new Error("value required");
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
365
|
+
return onPage(async (page) => {
|
|
366
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
367
|
+
await page.select(selector, String(value));
|
|
368
|
+
return { ok: true, selector, value };
|
|
369
|
+
});
|
|
281
370
|
}
|
|
282
371
|
|
|
283
372
|
export async function browser_hover({ selector } = {}) {
|
|
284
373
|
if (!selector) throw new Error("selector required");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
374
|
+
return onPage(async (page) => {
|
|
375
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
376
|
+
await page.hover(selector);
|
|
377
|
+
return { ok: true, selector };
|
|
378
|
+
});
|
|
289
379
|
}
|
|
290
380
|
|
|
291
381
|
export async function browser_evaluate({ code } = {}) {
|
|
292
382
|
if (!code) throw new Error("code required");
|
|
293
|
-
|
|
383
|
+
return onPage((page) => evaluateOnPage(page, code));
|
|
384
|
+
}
|
|
294
385
|
|
|
386
|
+
async function evaluateOnPage(page, code) {
|
|
295
387
|
// Install in-page console capture so evaluated code's logs come back.
|
|
296
388
|
await page.evaluate(() => {
|
|
297
389
|
window.__apxHelper = { logs: [], orig: { ...console } };
|
|
@@ -325,54 +417,56 @@ export async function browser_evaluate({ code } = {}) {
|
|
|
325
417
|
}
|
|
326
418
|
|
|
327
419
|
export async function browser_get_text({ selector } = {}) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
420
|
+
return onPage(async (page) => {
|
|
421
|
+
const text = await page.evaluate((sel) => {
|
|
422
|
+
const root = sel ? document.querySelector(sel) : document.body;
|
|
423
|
+
if (!root) return null;
|
|
424
|
+
const clone = root.cloneNode(true);
|
|
425
|
+
for (const tag of ["script", "style", "nav", "header", "footer", "noscript"]) {
|
|
426
|
+
for (const el of clone.querySelectorAll(tag)) el.remove();
|
|
427
|
+
}
|
|
428
|
+
return clone.innerText || clone.textContent || "";
|
|
429
|
+
}, selector ?? null);
|
|
430
|
+
if (text === null) throw new Error(`Element not found: ${selector}`);
|
|
431
|
+
const cleaned = text.replace(/\n{3,}/g, "\n\n").trim();
|
|
432
|
+
let title = "";
|
|
433
|
+
try { title = await page.title(); } catch { title = ""; }
|
|
434
|
+
return {
|
|
435
|
+
ok: true,
|
|
436
|
+
url: page.url(),
|
|
437
|
+
title,
|
|
438
|
+
text: cleaned,
|
|
439
|
+
chars: cleaned.length,
|
|
440
|
+
};
|
|
441
|
+
});
|
|
347
442
|
}
|
|
348
443
|
|
|
349
444
|
export async function browser_get_content({ selector } = {}) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
445
|
+
return onPage(async (page) => {
|
|
446
|
+
let content = selector
|
|
447
|
+
? await page.$eval(selector, el => el.innerHTML).catch(() => null)
|
|
448
|
+
: await page.content();
|
|
449
|
+
if (content === null) throw new Error(`Element not found: ${selector}`);
|
|
450
|
+
|
|
451
|
+
let truncated = false;
|
|
452
|
+
if (content.length > MAX_CONTENT_CHARS) {
|
|
453
|
+
content = content.slice(0, MAX_CONTENT_CHARS) + "\n[TRUNCATED]";
|
|
454
|
+
truncated = true;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
ok: true,
|
|
458
|
+
url: page.url(),
|
|
459
|
+
selector: selector ?? null,
|
|
460
|
+
chars: content.length,
|
|
461
|
+
truncated,
|
|
462
|
+
html: content,
|
|
463
|
+
};
|
|
464
|
+
});
|
|
369
465
|
}
|
|
370
466
|
|
|
371
467
|
export async function browser_wait_for_selector({ selector, timeout = 30000 } = {}) {
|
|
372
468
|
if (!selector) throw new Error("selector required");
|
|
373
|
-
|
|
374
|
-
await page.waitForSelector(selector, { timeout });
|
|
375
|
-
return { ok: true, selector };
|
|
469
|
+
return onPage((page) => page.waitForSelector(selector, { timeout }).then(() => ({ ok: true, selector })));
|
|
376
470
|
}
|
|
377
471
|
|
|
378
472
|
export async function browser_close() {
|