synabun 1.3.0 → 2.0.0-beta
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 +197 -662
- package/LICENSE-COMMERCIAL.md +33 -0
- package/NOTICE +17 -0
- package/README.md +175 -853
- package/THIRD-PARTY-LICENSES.md +131 -0
- package/benchmarks/browser-live-results.json +79 -0
- package/benchmarks/browser-v2-results.json +258 -0
- package/benchmarks/fixtures/browser/README.md +57 -0
- package/benchmarks/fixtures/browser/page-state.json +132 -0
- package/benchmarks/fixtures/browser/social.json +92 -0
- package/benchmarks/fixtures/browser/target.json +319 -0
- package/benchmarks/fixtures/desktop/README.md +58 -0
- package/benchmarks/fixtures/desktop/abstention.json +59 -0
- package/benchmarks/fixtures/desktop/adversarial.json +22 -0
- package/benchmarks/fixtures/desktop/forbidden.json +93 -0
- package/benchmarks/fixtures/desktop/generate.mjs +1023 -0
- package/benchmarks/fixtures/desktop/target.json +136 -0
- package/bin/synabun.js +30 -0
- package/docs/browser-v2.md +109 -0
- package/docs/hook-integrity.md +89 -0
- package/docs/judgments.md +182 -0
- package/docs/style-guide.md +246 -0
- package/docs/whatsapp.md +325 -0
- package/hooks/claude-code/post-plan.mjs +613 -391
- package/hooks/claude-code/post-remember.mjs +246 -122
- package/hooks/claude-code/pre-compact.mjs +171 -110
- package/hooks/claude-code/pre-task.mjs +133 -0
- package/hooks/claude-code/pre-websearch.mjs +15 -4
- package/hooks/claude-code/prompt-origin.mjs +94 -0
- package/hooks/claude-code/prompt-submit.mjs +1244 -861
- package/hooks/claude-code/session-start.mjs +114 -156
- package/hooks/claude-code/shared.mjs +1228 -368
- package/hooks/claude-code/state.mjs +167 -0
- package/hooks/claude-code/stop.mjs +515 -140
- package/hooks/claude-code/subagent-start.mjs +154 -0
- package/hooks/claude-code/subagent-stop.mjs +102 -0
- package/hooks/claude-code/task-gate.mjs +150 -0
- package/lib/claude-hooks.js +84 -0
- package/lib/claude-usage-audit.js +141 -0
- package/lib/client-config-repair.js +144 -0
- package/lib/data-home-migration.js +498 -0
- package/lib/first-launch-protection.js +240 -0
- package/lib/paths.d.ts +21 -0
- package/lib/paths.js +103 -0
- package/mcp-server/dist/config.d.ts +22 -0
- package/mcp-server/dist/config.d.ts.map +1 -0
- package/mcp-server/dist/config.js +109 -0
- package/mcp-server/dist/config.js.map +1 -0
- package/mcp-server/dist/http.d.ts +30 -0
- package/mcp-server/dist/http.d.ts.map +1 -0
- package/mcp-server/dist/http.js +276 -0
- package/mcp-server/dist/http.js.map +1 -0
- package/mcp-server/dist/index.d.ts +48 -0
- package/mcp-server/dist/index.d.ts.map +1 -0
- package/mcp-server/dist/index.js +538 -0
- package/mcp-server/dist/index.js.map +1 -0
- package/mcp-server/dist/preload-http.d.ts +3 -0
- package/mcp-server/dist/preload-http.d.ts.map +1 -0
- package/mcp-server/dist/preload-http.js +28 -0
- package/mcp-server/dist/preload-http.js.map +1 -0
- package/mcp-server/dist/preload.d.ts +3 -0
- package/mcp-server/dist/preload.d.ts.map +1 -0
- package/mcp-server/dist/preload.js +32 -0
- package/mcp-server/dist/preload.js.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts +3 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.js +15 -0
- package/mcp-server/dist/scripts/setup-embeddings.js.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts +79 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.js +128 -0
- package/mcp-server/dist/services/assistant-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts +314 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.js +344 -0
- package/mcp-server/dist/services/browser-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/browser-assist.d.ts +104 -0
- package/mcp-server/dist/services/browser-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist.js +282 -0
- package/mcp-server/dist/services/browser-assist.js.map +1 -0
- package/mcp-server/dist/services/browser-bench.d.ts +772 -0
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-bench.js +424 -0
- package/mcp-server/dist/services/browser-bench.js.map +1 -0
- package/mcp-server/dist/services/browser-judgments.d.ts +196 -0
- package/mcp-server/dist/services/browser-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-judgments.js +363 -0
- package/mcp-server/dist/services/browser-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-risk.d.ts +166 -0
- package/mcp-server/dist/services/browser-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-risk.js +458 -0
- package/mcp-server/dist/services/browser-risk.js.map +1 -0
- package/mcp-server/dist/services/categories.d.ts +61 -0
- package/mcp-server/dist/services/categories.d.ts.map +1 -0
- package/mcp-server/dist/services/categories.js +377 -0
- package/mcp-server/dist/services/categories.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +49 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.js +346 -0
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts +198 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.js +202 -0
- package/mcp-server/dist/services/desktop-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist.d.ts +104 -0
- package/mcp-server/dist/services/desktop-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist.js +266 -0
- package/mcp-server/dist/services/desktop-assist.js.map +1 -0
- package/mcp-server/dist/services/desktop-bench.d.ts +553 -0
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-bench.js +311 -0
- package/mcp-server/dist/services/desktop-bench.js.map +1 -0
- package/mcp-server/dist/services/desktop-client.d.ts +23 -0
- package/mcp-server/dist/services/desktop-client.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-client.js +79 -0
- package/mcp-server/dist/services/desktop-client.js.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts +125 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.js +177 -0
- package/mcp-server/dist/services/desktop-judgments.js.map +1 -0
- package/mcp-server/dist/services/desktop-risk.d.ts +283 -0
- package/mcp-server/dist/services/desktop-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-risk.js +605 -0
- package/mcp-server/dist/services/desktop-risk.js.map +1 -0
- package/mcp-server/dist/services/discord.d.ts +112 -0
- package/mcp-server/dist/services/discord.d.ts.map +1 -0
- package/mcp-server/dist/services/discord.js +477 -0
- package/mcp-server/dist/services/discord.js.map +1 -0
- package/mcp-server/dist/services/edit-stale.d.ts +81 -0
- package/mcp-server/dist/services/edit-stale.d.ts.map +1 -0
- package/mcp-server/dist/services/edit-stale.js +358 -0
- package/mcp-server/dist/services/edit-stale.js.map +1 -0
- package/mcp-server/dist/services/embedding-service.d.ts +16 -0
- package/mcp-server/dist/services/embedding-service.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-service.js +126 -0
- package/mcp-server/dist/services/embedding-service.js.map +1 -0
- package/mcp-server/dist/services/embedding-worker.d.ts +2 -0
- package/mcp-server/dist/services/embedding-worker.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-worker.js +52 -0
- package/mcp-server/dist/services/embedding-worker.js.map +1 -0
- package/mcp-server/dist/services/fb-regions.d.ts +71 -0
- package/mcp-server/dist/services/fb-regions.d.ts.map +1 -0
- package/mcp-server/dist/services/fb-regions.js +211 -0
- package/mcp-server/dist/services/fb-regions.js.map +1 -0
- package/mcp-server/dist/services/file-checksums.d.ts +22 -0
- package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
- package/mcp-server/dist/services/file-checksums.js +55 -0
- package/mcp-server/dist/services/file-checksums.js.map +1 -0
- package/mcp-server/dist/services/file-excerpt.d.ts +24 -0
- package/mcp-server/dist/services/file-excerpt.d.ts.map +1 -0
- package/mcp-server/dist/services/file-excerpt.js +65 -0
- package/mcp-server/dist/services/file-excerpt.js.map +1 -0
- package/mcp-server/dist/services/identity.d.ts +124 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -0
- package/mcp-server/dist/services/identity.js +241 -0
- package/mcp-server/dist/services/identity.js.map +1 -0
- package/mcp-server/dist/services/local-embeddings.d.ts +2 -0
- package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/services/local-embeddings.js +3 -0
- package/mcp-server/dist/services/local-embeddings.js.map +1 -0
- package/mcp-server/dist/services/memory-client.d.ts +4 -0
- package/mcp-server/dist/services/memory-client.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-client.js +27 -0
- package/mcp-server/dist/services/memory-client.js.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +135 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.js +510 -0
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -0
- package/mcp-server/dist/services/memory-judgments.d.ts +362 -0
- package/mcp-server/dist/services/memory-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judgments.js +674 -0
- package/mcp-server/dist/services/memory-judgments.js.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts +108 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.js +403 -0
- package/mcp-server/dist/services/memory-maintenance.js.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts +66 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.js +412 -0
- package/mcp-server/dist/services/memory-retrieval.js.map +1 -0
- package/mcp-server/dist/services/memory-schema.d.ts +16 -0
- package/mcp-server/dist/services/memory-schema.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-schema.js +282 -0
- package/mcp-server/dist/services/memory-schema.js.map +1 -0
- package/mcp-server/dist/services/memory-sessions.d.ts +24 -0
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-sessions.js +110 -0
- package/mcp-server/dist/services/memory-sessions.js.map +1 -0
- package/mcp-server/dist/services/memory-triage.d.ts +60 -0
- package/mcp-server/dist/services/memory-triage.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-triage.js +189 -0
- package/mcp-server/dist/services/memory-triage.js.map +1 -0
- package/mcp-server/dist/services/memory-verification.d.ts +24 -0
- package/mcp-server/dist/services/memory-verification.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-verification.js +92 -0
- package/mcp-server/dist/services/memory-verification.js.map +1 -0
- package/mcp-server/dist/services/memory-writer.d.ts +26 -0
- package/mcp-server/dist/services/memory-writer.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-writer.js +126 -0
- package/mcp-server/dist/services/memory-writer.js.map +1 -0
- package/mcp-server/dist/services/neural-interface.d.ts +439 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
- package/mcp-server/dist/services/neural-interface.js +1135 -0
- package/mcp-server/dist/services/neural-interface.js.map +1 -0
- package/mcp-server/dist/services/profiles.d.ts +120 -0
- package/mcp-server/dist/services/profiles.d.ts.map +1 -0
- package/mcp-server/dist/services/profiles.js +387 -0
- package/mcp-server/dist/services/profiles.js.map +1 -0
- package/mcp-server/dist/services/secret-gate.d.ts +29 -0
- package/mcp-server/dist/services/secret-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/secret-gate.js +43 -0
- package/mcp-server/dist/services/secret-gate.js.map +1 -0
- package/mcp-server/dist/services/server-instructions.d.ts +10 -0
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -0
- package/mcp-server/dist/services/server-instructions.js +101 -0
- package/mcp-server/dist/services/server-instructions.js.map +1 -0
- package/mcp-server/dist/services/sqlite.d.ts +235 -0
- package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
- package/mcp-server/dist/services/sqlite.js +1185 -0
- package/mcp-server/dist/services/sqlite.js.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts +99 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.js +218 -0
- package/mcp-server/dist/services/typesafe-bench.js.map +1 -0
- package/mcp-server/dist/services/typesafe-config.d.ts +342 -0
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-config.js +746 -0
- package/mcp-server/dist/services/typesafe-config.js.map +1 -0
- package/mcp-server/dist/services/typesafe.d.ts +213 -0
- package/mcp-server/dist/services/typesafe.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe.js +330 -0
- package/mcp-server/dist/services/typesafe.js.map +1 -0
- package/mcp-server/dist/services/vector-cache.d.ts +68 -0
- package/mcp-server/dist/services/vector-cache.d.ts.map +1 -0
- package/mcp-server/dist/services/vector-cache.js +271 -0
- package/mcp-server/dist/services/vector-cache.js.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +305 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.js +666 -0
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -0
- package/mcp-server/dist/tools/agents.d.ts +212 -0
- package/mcp-server/dist/tools/agents.d.ts.map +1 -0
- package/mcp-server/dist/tools/agents.js +407 -0
- package/mcp-server/dist/tools/agents.js.map +1 -0
- package/mcp-server/dist/tools/bluesky.d.ts +39 -0
- package/mcp-server/dist/tools/bluesky.d.ts.map +1 -0
- package/mcp-server/dist/tools/bluesky.js +1144 -0
- package/mcp-server/dist/tools/bluesky.js.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.js +106 -0
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
- package/mcp-server/dist/tools/browser-batch.d.ts +99 -0
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-batch.js +187 -0
- package/mcp-server/dist/tools/browser-batch.js.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js +144 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
- package/mcp-server/dist/tools/browser-interact.d.ts +239 -0
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-interact.js +300 -0
- package/mcp-server/dist/tools/browser-interact.js.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts +90 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.js +142 -0
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
- package/mcp-server/dist/tools/browser-observe.d.ts +561 -0
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-observe.js +2009 -0
- package/mcp-server/dist/tools/browser-observe.js.map +1 -0
- package/mcp-server/dist/tools/browser.d.ts +34 -0
- package/mcp-server/dist/tools/browser.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser.js +112 -0
- package/mcp-server/dist/tools/browser.js.map +1 -0
- package/mcp-server/dist/tools/card-tools.d.ts +82 -0
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/card-tools.js +137 -0
- package/mcp-server/dist/tools/card-tools.js.map +1 -0
- package/mcp-server/dist/tools/card.d.ts +8 -0
- package/mcp-server/dist/tools/card.d.ts.map +1 -0
- package/mcp-server/dist/tools/card.js +16 -0
- package/mcp-server/dist/tools/card.js.map +1 -0
- package/mcp-server/dist/tools/category.d.ts +32 -0
- package/mcp-server/dist/tools/category.d.ts.map +1 -0
- package/mcp-server/dist/tools/category.js +290 -0
- package/mcp-server/dist/tools/category.js.map +1 -0
- package/mcp-server/dist/tools/choice.d.ts +160 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -0
- package/mcp-server/dist/tools/choice.js +80 -0
- package/mcp-server/dist/tools/choice.js.map +1 -0
- package/mcp-server/dist/tools/computer.d.ts +71 -0
- package/mcp-server/dist/tools/computer.d.ts.map +1 -0
- package/mcp-server/dist/tools/computer.js +174 -0
- package/mcp-server/dist/tools/computer.js.map +1 -0
- package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-channel.js +236 -0
- package/mcp-server/dist/tools/discord-channel.js.map +1 -0
- package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-guild.js +138 -0
- package/mcp-server/dist/tools/discord-guild.js.map +1 -0
- package/mcp-server/dist/tools/discord-member.d.ts +26 -0
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-member.js +122 -0
- package/mcp-server/dist/tools/discord-member.js.map +1 -0
- package/mcp-server/dist/tools/discord-message.d.ts +36 -0
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-message.js +184 -0
- package/mcp-server/dist/tools/discord-message.js.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.js +179 -0
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
- package/mcp-server/dist/tools/discord-role.d.ts +34 -0
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-role.js +178 -0
- package/mcp-server/dist/tools/discord-role.js.map +1 -0
- package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-thread.js +88 -0
- package/mcp-server/dist/tools/discord-thread.js.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.js +154 -0
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
- package/mcp-server/dist/tools/discord.d.ts +7 -0
- package/mcp-server/dist/tools/discord.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord.js +25 -0
- package/mcp-server/dist/tools/discord.js.map +1 -0
- package/mcp-server/dist/tools/fb-groups.d.ts +98 -0
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/fb-groups.js +369 -0
- package/mcp-server/dist/tools/fb-groups.js.map +1 -0
- package/mcp-server/dist/tools/forget.d.ts +14 -0
- package/mcp-server/dist/tools/forget.d.ts.map +1 -0
- package/mcp-server/dist/tools/forget.js +26 -0
- package/mcp-server/dist/tools/forget.js.map +1 -0
- package/mcp-server/dist/tools/git-tools.d.ts +24 -0
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/git-tools.js +107 -0
- package/mcp-server/dist/tools/git-tools.js.map +1 -0
- package/mcp-server/dist/tools/git.d.ts +7 -0
- package/mcp-server/dist/tools/git.d.ts.map +1 -0
- package/mcp-server/dist/tools/git.js +11 -0
- package/mcp-server/dist/tools/git.js.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.js +351 -0
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.js +258 -0
- package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.js +357 -0
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.js +321 -0
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.js +197 -0
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.js +274 -0
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.js +425 -0
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
- package/mcp-server/dist/tools/gsc.d.ts +7 -0
- package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc.js +51 -0
- package/mcp-server/dist/tools/gsc.js.map +1 -0
- package/mcp-server/dist/tools/image-tools.d.ts +24 -0
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/image-tools.js +80 -0
- package/mcp-server/dist/tools/image-tools.js.map +1 -0
- package/mcp-server/dist/tools/image.d.ts +10 -0
- package/mcp-server/dist/tools/image.d.ts.map +1 -0
- package/mcp-server/dist/tools/image.js +14 -0
- package/mcp-server/dist/tools/image.js.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
- package/mcp-server/dist/tools/leonardo.d.ts +13 -0
- package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo.js +21 -0
- package/mcp-server/dist/tools/leonardo.js.map +1 -0
- package/mcp-server/dist/tools/loop.d.ts +30 -0
- package/mcp-server/dist/tools/loop.d.ts.map +1 -0
- package/mcp-server/dist/tools/loop.js +401 -0
- package/mcp-server/dist/tools/loop.js.map +1 -0
- package/mcp-server/dist/tools/memories.d.ts +56 -0
- package/mcp-server/dist/tools/memories.d.ts.map +1 -0
- package/mcp-server/dist/tools/memories.js +171 -0
- package/mcp-server/dist/tools/memories.js.map +1 -0
- package/mcp-server/dist/tools/morelogin.d.ts +24 -0
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -0
- package/mcp-server/dist/tools/morelogin.js +79 -0
- package/mcp-server/dist/tools/morelogin.js.map +1 -0
- package/mcp-server/dist/tools/profile.d.ts +17 -0
- package/mcp-server/dist/tools/profile.d.ts.map +1 -0
- package/mcp-server/dist/tools/profile.js +114 -0
- package/mcp-server/dist/tools/profile.js.map +1 -0
- package/mcp-server/dist/tools/recall.d.ts +71 -0
- package/mcp-server/dist/tools/recall.d.ts.map +1 -0
- package/mcp-server/dist/tools/recall.js +147 -0
- package/mcp-server/dist/tools/recall.js.map +1 -0
- package/mcp-server/dist/tools/reflect.d.ts +56 -0
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
- package/mcp-server/dist/tools/reflect.js +157 -0
- package/mcp-server/dist/tools/reflect.js.map +1 -0
- package/mcp-server/dist/tools/remember.d.ts +62 -0
- package/mcp-server/dist/tools/remember.d.ts.map +1 -0
- package/mcp-server/dist/tools/remember.js +201 -0
- package/mcp-server/dist/tools/remember.js.map +1 -0
- package/mcp-server/dist/tools/response.d.ts +23 -0
- package/mcp-server/dist/tools/response.d.ts.map +1 -0
- package/mcp-server/dist/tools/response.js +24 -0
- package/mcp-server/dist/tools/response.js.map +1 -0
- package/mcp-server/dist/tools/restore.d.ts +14 -0
- package/mcp-server/dist/tools/restore.d.ts.map +1 -0
- package/mcp-server/dist/tools/restore.js +23 -0
- package/mcp-server/dist/tools/restore.js.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts +51 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.js +231 -0
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -0
- package/mcp-server/dist/tools/style-guide.d.ts +8 -0
- package/mcp-server/dist/tools/style-guide.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide.js +12 -0
- package/mcp-server/dist/tools/style-guide.js.map +1 -0
- package/mcp-server/dist/tools/sync.d.ts +20 -0
- package/mcp-server/dist/tools/sync.d.ts.map +1 -0
- package/mcp-server/dist/tools/sync.js +156 -0
- package/mcp-server/dist/tools/sync.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
- package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe.js +11 -0
- package/mcp-server/dist/tools/tictactoe.js.map +1 -0
- package/mcp-server/dist/tools/utils.d.ts +19 -0
- package/mcp-server/dist/tools/utils.d.ts.map +1 -0
- package/mcp-server/dist/tools/utils.js +54 -0
- package/mcp-server/dist/tools/utils.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts +88 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.js +337 -0
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +326 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.js +222 -0
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
- package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard.js +16 -0
- package/mcp-server/dist/tools/whiteboard.js.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts +46 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.js +83 -0
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts +35 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.js +56 -0
- package/mcp-server/dist/tools/youtube/dedup.js.map +1 -0
- package/mcp-server/dist/tools/youtube/download.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/download.js +30 -0
- package/mcp-server/dist/tools/youtube/download.js.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts +66 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.js +143 -0
- package/mcp-server/dist/tools/youtube/helpers.js.map +1 -0
- package/mcp-server/dist/tools/youtube/media.d.ts +43 -0
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/media.js +97 -0
- package/mcp-server/dist/tools/youtube/media.js.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts +33 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.js +106 -0
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts +30 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.js +71 -0
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts +29 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.js +128 -0
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.js +147 -0
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -0
- package/mcp-server/dist/tools/youtube/source.d.ts +36 -0
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/source.js +85 -0
- package/mcp-server/dist/tools/youtube/source.js.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts +53 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.js +255 -0
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -0
- package/mcp-server/dist/tools/youtube.d.ts +12 -0
- package/mcp-server/dist/tools/youtube.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube.js +38 -0
- package/mcp-server/dist/tools/youtube.js.map +1 -0
- package/mcp-server/dist/tui.d.ts +3 -0
- package/mcp-server/dist/tui.d.ts.map +1 -0
- package/mcp-server/dist/tui.js +806 -0
- package/mcp-server/dist/tui.js.map +1 -0
- package/mcp-server/dist/types.d.ts +77 -0
- package/mcp-server/dist/types.d.ts.map +1 -0
- package/mcp-server/dist/types.js +2 -0
- package/mcp-server/dist/types.js.map +1 -0
- package/mcp-server/package-lock.json +4798 -0
- package/mcp-server/package.json +55 -38
- package/mcp-server/run.mjs +12 -2
- package/mcp-server/src/config.ts +48 -3
- package/mcp-server/src/http.ts +296 -93
- package/mcp-server/src/index.ts +546 -203
- package/mcp-server/src/preload-http.ts +19 -18
- package/mcp-server/src/preload.ts +19 -18
- package/mcp-server/src/scripts/setup-embeddings.ts +9 -0
- package/mcp-server/src/services/assistant-judgments.ts +157 -0
- package/mcp-server/src/services/browser-assist-gate.ts +383 -0
- package/mcp-server/src/services/browser-assist.ts +267 -0
- package/mcp-server/src/services/browser-bench.ts +477 -0
- package/mcp-server/src/services/browser-judgments.ts +414 -0
- package/mcp-server/src/services/browser-risk.ts +484 -0
- package/mcp-server/src/services/categories.ts +36 -19
- package/mcp-server/src/services/codex-config-heal.ts +403 -0
- package/mcp-server/src/services/desktop-assist-gate.ts +251 -0
- package/mcp-server/src/services/desktop-assist.ts +308 -0
- package/mcp-server/src/services/desktop-bench.ts +372 -0
- package/mcp-server/src/services/desktop-client.ts +76 -0
- package/mcp-server/src/services/desktop-judgments.ts +218 -0
- package/mcp-server/src/services/desktop-risk.ts +743 -0
- package/mcp-server/src/services/edit-stale.ts +299 -0
- package/mcp-server/src/services/embedding-service.ts +108 -0
- package/mcp-server/src/services/embedding-worker.ts +46 -0
- package/mcp-server/src/services/fb-regions.ts +221 -0
- package/mcp-server/src/services/file-checksums.ts +26 -9
- package/mcp-server/src/services/file-excerpt.ts +59 -0
- package/mcp-server/src/services/identity.ts +291 -0
- package/mcp-server/src/services/local-embeddings.ts +2 -77
- package/mcp-server/src/services/memory-client.ts +23 -0
- package/mcp-server/src/services/memory-judge-backfill.ts +462 -0
- package/mcp-server/src/services/memory-judgments.ts +901 -0
- package/mcp-server/src/services/memory-maintenance.ts +360 -0
- package/mcp-server/src/services/memory-retrieval.ts +349 -0
- package/mcp-server/src/services/memory-schema.ts +280 -0
- package/mcp-server/src/services/memory-sessions.ts +87 -0
- package/mcp-server/src/services/memory-triage.ts +182 -0
- package/mcp-server/src/services/memory-verification.ts +68 -0
- package/mcp-server/src/services/memory-writer.ts +125 -0
- package/mcp-server/src/services/neural-interface.ts +1217 -73
- package/mcp-server/src/services/profiles.ts +428 -0
- package/mcp-server/src/services/secret-gate.ts +52 -0
- package/mcp-server/src/services/server-instructions.ts +122 -0
- package/mcp-server/src/services/sqlite.ts +1456 -823
- package/mcp-server/src/services/typesafe-bench.ts +247 -0
- package/mcp-server/src/services/typesafe-config.ts +790 -0
- package/mcp-server/src/services/typesafe.ts +390 -0
- package/mcp-server/src/services/vector-cache.ts +255 -0
- package/mcp-server/src/services/whiteboard-geometry.ts +711 -0
- package/mcp-server/src/tools/agents.ts +539 -0
- package/mcp-server/src/tools/bluesky.ts +1186 -0
- package/mcp-server/src/tools/browser-advanced.ts +15 -6
- package/mcp-server/src/tools/browser-batch.ts +178 -0
- package/mcp-server/src/tools/browser-cheatsheet.ts +155 -0
- package/mcp-server/src/tools/browser-interact.ts +362 -255
- package/mcp-server/src/tools/browser-navigate.ts +164 -77
- package/mcp-server/src/tools/browser-observe.ts +2222 -1330
- package/mcp-server/src/tools/browser.ts +181 -107
- package/mcp-server/src/tools/card.ts +7 -5
- package/mcp-server/src/tools/choice.ts +103 -0
- package/mcp-server/src/tools/computer.ts +190 -0
- package/mcp-server/src/tools/discord.ts +10 -23
- package/mcp-server/src/tools/fb-groups.ts +387 -0
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git.ts +3 -1
- package/mcp-server/src/tools/gsc/experience.ts +440 -0
- package/mcp-server/src/tools/gsc/helpers.ts +288 -0
- package/mcp-server/src/tools/gsc/indexing.ts +495 -0
- package/mcp-server/src/tools/gsc/inspect.ts +391 -0
- package/mcp-server/src/tools/gsc/navigate.ts +236 -0
- package/mcp-server/src/tools/gsc/performance.ts +352 -0
- package/mcp-server/src/tools/gsc/settings.ts +564 -0
- package/mcp-server/src/tools/gsc.ts +95 -0
- package/mcp-server/src/tools/image.ts +3 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
- package/mcp-server/src/tools/leonardo.ts +7 -5
- package/mcp-server/src/tools/loop.ts +118 -25
- package/mcp-server/src/tools/memories.ts +65 -5
- package/mcp-server/src/tools/morelogin.ts +81 -0
- package/mcp-server/src/tools/profile.ts +122 -0
- package/mcp-server/src/tools/recall.ts +105 -261
- package/mcp-server/src/tools/reflect.ts +171 -145
- package/mcp-server/src/tools/remember.ts +223 -115
- package/mcp-server/src/tools/style-guide-tools.ts +250 -0
- package/mcp-server/src/tools/style-guide.ts +16 -0
- package/mcp-server/src/tools/sync.ts +66 -9
- package/mcp-server/src/tools/tictactoe.ts +3 -1
- package/mcp-server/src/tools/utils.ts +22 -0
- package/mcp-server/src/tools/whiteboard-format.ts +394 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +191 -183
- package/mcp-server/src/tools/whiteboard.ts +7 -5
- package/mcp-server/src/tools/youtube/analytics.ts +106 -0
- package/mcp-server/src/tools/youtube/dedup.ts +57 -0
- package/mcp-server/src/tools/youtube/download.ts +41 -0
- package/mcp-server/src/tools/youtube/helpers.ts +183 -0
- package/mcp-server/src/tools/youtube/media.ts +109 -0
- package/mcp-server/src/tools/youtube/metadata.ts +112 -0
- package/mcp-server/src/tools/youtube/navigate.ts +81 -0
- package/mcp-server/src/tools/youtube/playlist.ts +134 -0
- package/mcp-server/src/tools/youtube/schedule.ts +158 -0
- package/mcp-server/src/tools/youtube/source.ts +101 -0
- package/mcp-server/src/tools/youtube/upload.ts +296 -0
- package/mcp-server/src/tools/youtube.ts +47 -0
- package/mcp-server/src/tui.ts +8 -9
- package/neural-interface/games/TicTacToe/1.svg +1 -0
- package/neural-interface/games/TicTacToe/2.svg +1 -0
- package/neural-interface/games/TicTacToe/3.svg +1 -0
- package/neural-interface/games/TicTacToe/4.svg +1 -0
- package/neural-interface/games/TicTacToe/5.svg +1 -0
- package/neural-interface/games/TicTacToe/6.svg +1 -0
- package/neural-interface/games/TicTacToe/7.svg +1 -0
- package/neural-interface/games/TicTacToe/8.svg +1 -0
- package/neural-interface/games/TicTacToe/9.svg +1 -0
- package/neural-interface/games/TicTacToe/Board.svg +1 -0
- package/neural-interface/games/TicTacToe/Circle.svg +1 -0
- package/neural-interface/games/TicTacToe/Cross.svg +1 -0
- package/neural-interface/i18n/en.json +6983 -0
- package/neural-interface/i18n/pt-BR.json +6987 -0
- package/neural-interface/lib/assistant-api.js +1005 -0
- package/neural-interface/lib/assistant-attachments.js +121 -0
- package/neural-interface/lib/assistant-brains/claude.js +207 -0
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +43 -0
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +56 -0
- package/neural-interface/lib/assistant-brains/codex.js +213 -0
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +47 -0
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +62 -0
- package/neural-interface/lib/assistant-brains/opencode.js +320 -0
- package/neural-interface/lib/assistant-budget.js +318 -0
- package/neural-interface/lib/assistant-catalog.js +731 -0
- package/neural-interface/lib/assistant-clarify.js +664 -0
- package/neural-interface/lib/assistant-config.js +232 -0
- package/neural-interface/lib/assistant-dispatch.js +2744 -0
- package/neural-interface/lib/assistant-envelope.js +579 -0
- package/neural-interface/lib/assistant-evidence.js +236 -0
- package/neural-interface/lib/assistant-hidden-models.js +272 -0
- package/neural-interface/lib/assistant-jev.js +117 -0
- package/neural-interface/lib/assistant-judgments.js +111 -0
- package/neural-interface/lib/assistant-media.js +335 -0
- package/neural-interface/lib/assistant-memory.js +398 -0
- package/neural-interface/lib/assistant-persona.js +444 -0
- package/neural-interface/lib/assistant-plan-permissions.js +448 -0
- package/neural-interface/lib/assistant-playbooks/design.md +87 -0
- package/neural-interface/lib/assistant-playbooks.js +72 -0
- package/neural-interface/lib/assistant-pricing.js +159 -0
- package/neural-interface/lib/assistant-route-gate-codex.js +163 -0
- package/neural-interface/lib/assistant-route-gate.js +212 -0
- package/neural-interface/lib/assistant-router.js +1324 -0
- package/neural-interface/lib/assistant-runtime.js +3337 -0
- package/neural-interface/lib/assistant-task-prompt.js +472 -0
- package/neural-interface/lib/assistant-usage.js +1688 -0
- package/neural-interface/lib/augmented-path.js +50 -0
- package/neural-interface/lib/automation-storage-migration.js +54 -0
- package/neural-interface/lib/backup-service.js +851 -0
- package/neural-interface/lib/backup-zip-reader.js +262 -0
- package/neural-interface/lib/browser-assist-api.js +41 -0
- package/neural-interface/lib/browser-console.js +176 -0
- package/neural-interface/lib/browser-execution.js +241 -0
- package/neural-interface/lib/browser-extraction.js +191 -0
- package/neural-interface/lib/browser-screenshot.js +399 -0
- package/neural-interface/lib/browser-semantic-context.js +639 -0
- package/neural-interface/lib/browser-snapshots.js +192 -0
- package/neural-interface/lib/browser-tab-recovery.js +119 -0
- package/neural-interface/lib/browser-targets.js +70 -0
- package/neural-interface/lib/browser-tool-policy.js +926 -0
- package/neural-interface/lib/claude-accounts.js +230 -0
- package/neural-interface/lib/claude-agent-bridge.js +3296 -0
- package/neural-interface/lib/claude-bypass-policy.js +99 -0
- package/neural-interface/lib/claude-history.js +429 -0
- package/neural-interface/lib/claude-model-catalog.js +483 -0
- package/neural-interface/lib/claude-panel-requests.js +165 -0
- package/neural-interface/lib/claude-panel-session.js +272 -0
- package/neural-interface/lib/claude-permission-rules.js +153 -0
- package/neural-interface/lib/claude-session-cleanup.js +71 -0
- package/neural-interface/lib/claude-session-list.js +82 -0
- package/neural-interface/lib/claude-session-ops.js +140 -0
- package/neural-interface/lib/claude-settings-view.js +51 -0
- package/neural-interface/lib/claude-skin-connect.js +79 -0
- package/neural-interface/lib/claude-stall-policy.js +7 -0
- package/neural-interface/lib/claude-temporary.js +435 -0
- package/neural-interface/lib/cli-update-planner.js +312 -0
- package/neural-interface/lib/codex-capabilities.d.ts +41 -0
- package/neural-interface/lib/codex-capabilities.js +148 -0
- package/neural-interface/lib/codex-collaboration-mode.js +17 -0
- package/neural-interface/lib/codex-compat-rpc.js +241 -0
- package/neural-interface/lib/codex-mcp-approvals.js +56 -0
- package/neural-interface/lib/codex-mcp-apps.js +47 -0
- package/neural-interface/lib/codex-model-context.js +202 -0
- package/neural-interface/lib/codex-plan-store.js +71 -0
- package/neural-interface/lib/codex-request-journal.js +242 -0
- package/neural-interface/lib/codex-runtime-path.js +213 -0
- package/neural-interface/lib/codex-session-catalog.js +231 -0
- package/neural-interface/lib/codex-session-settings.js +46 -0
- package/neural-interface/lib/codex-sidepanel-permissions.js +110 -0
- package/neural-interface/lib/codex-thread-history.js +165 -0
- package/neural-interface/lib/codex-user-verification.js +34 -0
- package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
- package/neural-interface/lib/confined-fs.js +256 -0
- package/neural-interface/lib/db.js +922 -0
- package/neural-interface/lib/design-md.js +9 -0
- package/neural-interface/lib/desktop/api.js +92 -0
- package/neural-interface/lib/desktop/audit.js +62 -0
- package/neural-interface/lib/desktop/build.js +423 -0
- package/neural-interface/lib/desktop/config.js +606 -0
- package/neural-interface/lib/desktop/coords.js +76 -0
- package/neural-interface/lib/desktop/fake-helper.js +1192 -0
- package/neural-interface/lib/desktop/frames.js +77 -0
- package/neural-interface/lib/desktop/grants.js +84 -0
- package/neural-interface/lib/desktop/guards.js +170 -0
- package/neural-interface/lib/desktop/helper/SynabunDesktop.swift +3056 -0
- package/neural-interface/lib/desktop/lease.js +51 -0
- package/neural-interface/lib/desktop/manager.js +545 -0
- package/neural-interface/lib/desktop/press-contexts.js +103 -0
- package/neural-interface/lib/desktop/protocol.js +495 -0
- package/neural-interface/lib/desktop/service.js +1202 -0
- package/neural-interface/lib/effort-levels.js +148 -0
- package/neural-interface/lib/event-loop-monitor.js +67 -0
- package/neural-interface/lib/exec-loop-command.js +209 -0
- package/neural-interface/lib/folder-picker.ps1 +77 -0
- package/neural-interface/lib/http-guards.js +287 -0
- package/neural-interface/lib/jsonl-lines.js +94 -0
- package/neural-interface/lib/loop-logger.js +127 -0
- package/neural-interface/lib/mcp-client-config.js +133 -0
- package/neural-interface/lib/mcp-installer.js +487 -0
- package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
- package/neural-interface/lib/mcp-profile-servers.js +120 -0
- package/neural-interface/lib/memo-cache.js +84 -0
- package/neural-interface/lib/memory-api.js +758 -0
- package/neural-interface/lib/memory-map-api.js +1150 -0
- package/neural-interface/lib/memory-map-layout.js +872 -0
- package/neural-interface/lib/memory-map-worker.js +165 -0
- package/neural-interface/lib/memory-session-capture.js +81 -0
- package/neural-interface/lib/memory-timers.js +62 -0
- package/neural-interface/lib/morelogin.js +301 -0
- package/neural-interface/lib/native-binary-runtime.js +650 -0
- package/neural-interface/lib/native-loop-goal.js +33 -0
- package/neural-interface/lib/native-loop-providers.js +1016 -0
- package/neural-interface/lib/native-loop-runtime.js +747 -0
- package/neural-interface/lib/opencode-hidden-models.js +113 -0
- package/neural-interface/lib/opencode-history.js +253 -0
- package/neural-interface/lib/opencode-runtime-config.js +58 -0
- package/neural-interface/lib/opencode-v2-client.js +486 -0
- package/neural-interface/lib/opencode-v2-ws-requests.js +955 -0
- package/neural-interface/lib/path-confine.js +213 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/lib/project-storage.js +766 -0
- package/neural-interface/lib/pty-host/core.js +603 -0
- package/neural-interface/lib/pty-host/flow.js +43 -0
- package/neural-interface/lib/pty-host/host.js +47 -0
- package/neural-interface/lib/pty-host/manager.js +360 -0
- package/neural-interface/lib/pty-host/protocol.js +81 -0
- package/neural-interface/lib/pty-host/snapshot.js +133 -0
- package/neural-interface/lib/remote-policy.js +582 -0
- package/neural-interface/lib/rulesets/api.js +175 -0
- package/neural-interface/lib/rulesets/fs-safe.js +245 -0
- package/neural-interface/lib/rulesets/installer.js +1453 -0
- package/neural-interface/lib/rulesets/json-edit.js +186 -0
- package/neural-interface/lib/rulesets/lock.js +170 -0
- package/neural-interface/lib/rulesets/managed-text.js +314 -0
- package/neural-interface/lib/rulesets/normalise.js +24 -0
- package/neural-interface/lib/rulesets/render.js +84 -0
- package/neural-interface/lib/schedule-launch-intent.js +40 -0
- package/neural-interface/lib/session-cache-builder.js +524 -0
- package/neural-interface/lib/session-chunker.js +344 -0
- package/neural-interface/lib/session-indexer.js +327 -0
- package/neural-interface/lib/session-title-generator.js +227 -0
- package/neural-interface/lib/skill-prompt-bridge.js +11 -0
- package/neural-interface/lib/style-guide/api.js +375 -0
- package/neural-interface/lib/style-guide/color.js +352 -0
- package/neural-interface/lib/style-guide/export-css.js +99 -0
- package/neural-interface/lib/style-guide/export-dtcg.js +209 -0
- package/neural-interface/lib/style-guide/export-tailwind.js +102 -0
- package/neural-interface/lib/style-guide/fonts.json +227 -0
- package/neural-interface/lib/style-guide/import.js +1268 -0
- package/neural-interface/lib/style-guide/pointers.js +103 -0
- package/neural-interface/lib/style-guide/presets.js +232 -0
- package/neural-interface/lib/style-guide/render-design-md.js +319 -0
- package/neural-interface/lib/style-guide/schema.js +848 -0
- package/neural-interface/lib/style-guide/security.js +43 -0
- package/neural-interface/lib/style-guide/store.js +491 -0
- package/neural-interface/lib/style-guide/summary.js +106 -0
- package/neural-interface/lib/style-guide/tokens.js +170 -0
- package/neural-interface/lib/synabun-update-plan.js +305 -0
- package/neural-interface/lib/synabun-version.js +179 -0
- package/neural-interface/lib/system-restore.js +99 -0
- package/neural-interface/lib/terminal-env.js +86 -0
- package/neural-interface/lib/ui-blob-store.js +106 -0
- package/neural-interface/lib/vector-cache.js +2 -0
- package/neural-interface/lib/virtual-socket.js +60 -0
- package/neural-interface/lib/whatsapp/api.js +146 -0
- package/neural-interface/lib/whatsapp/auth-store.js +389 -0
- package/neural-interface/lib/whatsapp/baileys-adapter.js +457 -0
- package/neural-interface/lib/whatsapp/brain.js +128 -0
- package/neural-interface/lib/whatsapp/bridge.js +1499 -0
- package/neural-interface/lib/whatsapp/cards.js +519 -0
- package/neural-interface/lib/whatsapp/commands.js +92 -0
- package/neural-interface/lib/whatsapp/config.js +291 -0
- package/neural-interface/lib/whatsapp/connector/entry.mjs +25 -0
- package/neural-interface/lib/whatsapp/connector/manifest.json +6 -0
- package/neural-interface/lib/whatsapp/connector/npm-shrinkwrap.json +717 -0
- package/neural-interface/lib/whatsapp/connector/package.json +8 -0
- package/neural-interface/lib/whatsapp/fake-baileys.js +444 -0
- package/neural-interface/lib/whatsapp/format.js +552 -0
- package/neural-interface/lib/whatsapp/host-core.js +1225 -0
- package/neural-interface/lib/whatsapp/host.js +119 -0
- package/neural-interface/lib/whatsapp/identity.js +288 -0
- package/neural-interface/lib/whatsapp/inbound.js +149 -0
- package/neural-interface/lib/whatsapp/installer.js +562 -0
- package/neural-interface/lib/whatsapp/manager.js +638 -0
- package/neural-interface/lib/whatsapp/paths.js +81 -0
- package/neural-interface/lib/whatsapp/protocol.js +327 -0
- package/neural-interface/lib/whatsapp/qr-svg.js +57 -0
- package/neural-interface/lib/whatsapp/redact.js +130 -0
- package/neural-interface/lib/whatsapp/service.js +1305 -0
- package/neural-interface/lib/whatsapp/vendor/qrcodegen.js +848 -0
- package/neural-interface/package-lock.json +4682 -0
- package/neural-interface/package.json +58 -27
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +1 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index.html +32 -182
- package/neural-interface/public/index2d.html +2 -2
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/mcp.svg +4 -0
- package/neural-interface/public/offline.html +335 -299
- package/neural-interface/public/onboarding.html +692 -155
- package/neural-interface/public/opencode-logo-dark-square.svg +1 -0
- package/neural-interface/public/shared/agent-runtime-options.js +596 -0
- package/neural-interface/public/shared/api.js +339 -28
- package/neural-interface/public/shared/assistant/asst-accounts.js +318 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +296 -0
- package/neural-interface/public/shared/assistant/asst-api.js +270 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +744 -0
- package/neural-interface/public/shared/assistant/asst-budget.js +333 -0
- package/neural-interface/public/shared/assistant/asst-composer.js +656 -0
- package/neural-interface/public/shared/assistant/asst-computer.js +803 -0
- package/neural-interface/public/shared/assistant/asst-control.js +1018 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +262 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +311 -0
- package/neural-interface/public/shared/assistant/asst-menu.js +375 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +1845 -0
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +589 -0
- package/neural-interface/public/shared/assistant/asst-render.js +3296 -0
- package/neural-interface/public/shared/assistant/asst-route.js +1653 -0
- package/neural-interface/public/shared/assistant/asst-roving.js +75 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +117 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +488 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +675 -0
- package/neural-interface/public/shared/assistant/asst-slash.js +258 -0
- package/neural-interface/public/shared/assistant/asst-state.js +727 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +1684 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +1017 -0
- package/neural-interface/public/shared/assistant/asst-usage.js +444 -0
- package/neural-interface/public/shared/assistant/asst-ws.js +175 -0
- package/neural-interface/public/shared/automation-context.js +75 -0
- package/neural-interface/public/shared/blob-store.js +99 -0
- package/neural-interface/public/shared/cdx/cdx-capabilities.js +118 -0
- package/neural-interface/public/shared/cdx/cdx-cards.js +37 -0
- package/neural-interface/public/shared/cdx/cdx-commands.js +171 -0
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +402 -0
- package/neural-interface/public/shared/cdx/cdx-context-model.js +220 -0
- package/neural-interface/public/shared/cdx/cdx-features.js +408 -0
- package/neural-interface/public/shared/cdx/cdx-icons.js +89 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-host.js +414 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.html +6 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.js +56 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app.js +178 -0
- package/neural-interface/public/shared/cdx/cdx-model-catalog.js +120 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +261 -0
- package/neural-interface/public/shared/cdx/cdx-output.js +382 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +1372 -0
- package/neural-interface/public/shared/cdx/cdx-plan.js +266 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +682 -0
- package/neural-interface/public/shared/cdx/cdx-render.js +3381 -0
- package/neural-interface/public/shared/cdx/cdx-requests.js +1136 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +3251 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +7992 -0
- package/neural-interface/public/shared/cdx/cdx-ws.js +363 -0
- package/neural-interface/public/shared/cli-status.js +169 -0
- package/neural-interface/public/shared/constants.js +22 -5
- package/neural-interface/public/shared/cp/cp-agents.js +186 -0
- package/neural-interface/public/shared/cp/cp-bash.js +252 -0
- package/neural-interface/public/shared/cp/cp-compose.js +159 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +309 -0
- package/neural-interface/public/shared/cp/cp-context-model.js +157 -0
- package/neural-interface/public/shared/cp/cp-context-window.js +80 -0
- package/neural-interface/public/shared/cp/cp-ctx.js +11 -0
- package/neural-interface/public/shared/cp/cp-diff.js +255 -0
- package/neural-interface/public/shared/cp/cp-event-rows.js +637 -0
- package/neural-interface/public/shared/cp/cp-events.js +846 -0
- package/neural-interface/public/shared/cp/cp-markdown.js +104 -0
- package/neural-interface/public/shared/cp/cp-permission-model.js +905 -0
- package/neural-interface/public/shared/cp/cp-permissions.js +626 -0
- package/neural-interface/public/shared/cp/cp-rehydrate.js +113 -0
- package/neural-interface/public/shared/cp/cp-restore.js +224 -0
- package/neural-interface/public/shared/cp/cp-session-model.js +338 -0
- package/neural-interface/public/shared/cp/cp-session-settings.js +254 -0
- package/neural-interface/public/shared/cp/cp-sessions.js +227 -0
- package/neural-interface/public/shared/cp/cp-statusline.js +153 -0
- package/neural-interface/public/shared/cp/cp-styles.js +349 -0
- package/neural-interface/public/shared/cp/cp-tasks-model.js +282 -0
- package/neural-interface/public/shared/cp/cp-tasks.js +107 -0
- package/neural-interface/public/shared/cp/cp-temporary-view.js +56 -0
- package/neural-interface/public/shared/cp/cp-temporary.js +231 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +326 -0
- package/neural-interface/public/shared/cp/cp-tool-results.js +618 -0
- package/neural-interface/public/shared/cp/cp-usage-model.js +124 -0
- package/neural-interface/public/shared/graph-activity.js +68 -0
- package/neural-interface/public/shared/html-shell.js +107 -12
- package/neural-interface/public/shared/i18n.js +132 -34
- package/neural-interface/public/shared/node-positions-store.js +44 -0
- package/neural-interface/public/shared/ocp-hidden-models.js +116 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +355 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-autoaccept-button.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-automation-ownership.js +72 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +280 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +67 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes.js +164 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +725 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +75 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +1204 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +252 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +478 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +233 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-icons.js +30 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +1065 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-mention-hints.js +182 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +655 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +2636 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +601 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-profile-inheritance.js +78 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +649 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +357 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render-logic.js +189 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1869 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-select.js +42 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +1714 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +468 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-widgets.js +63 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +366 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-settings-link.js +19 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +343 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +746 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +347 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +338 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +3543 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-subagents-logic.js +482 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools-logic.js +351 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools.js +168 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +315 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +545 -0
- package/neural-interface/public/shared/provider-icons.js +30 -0
- package/neural-interface/public/shared/session-title.js +100 -0
- package/neural-interface/public/shared/settings/settings-ia.js +389 -0
- package/neural-interface/public/shared/settings/settings-kit.js +234 -0
- package/neural-interface/public/shared/settings/settings-search.js +463 -0
- package/neural-interface/public/shared/settings/settings-shell.js +570 -0
- package/neural-interface/public/shared/state.js +5 -0
- package/neural-interface/public/shared/storage.js +50 -7
- package/neural-interface/public/shared/styleguide/sg-agents.js +42 -0
- package/neural-interface/public/shared/styleguide/sg-brand.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-color-client.js +388 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +148 -0
- package/neural-interface/public/shared/styleguide/sg-components.js +35 -0
- package/neural-interface/public/shared/styleguide/sg-history.js +41 -0
- package/neural-interface/public/shared/styleguide/sg-icons-logo.js +38 -0
- package/neural-interface/public/shared/styleguide/sg-imagery.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-import-export.js +47 -0
- package/neural-interface/public/shared/styleguide/sg-kit.js +281 -0
- package/neural-interface/public/shared/styleguide/sg-labels.js +318 -0
- package/neural-interface/public/shared/styleguide/sg-layout.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-motion.js +5 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +229 -0
- package/neural-interface/public/shared/styleguide/sg-preview.js +138 -0
- package/neural-interface/public/shared/styleguide/sg-rules.js +9 -0
- package/neural-interface/public/shared/styleguide/sg-shape.js +24 -0
- package/neural-interface/public/shared/styleguide/sg-state.js +132 -0
- package/neural-interface/public/shared/styleguide/sg-typography.js +124 -0
- package/neural-interface/public/shared/styles.css +7883 -1155
- package/neural-interface/public/shared/synabun-mascot.js +1625 -0
- package/neural-interface/public/shared/synabun-ticker.js +376 -0
- package/neural-interface/public/shared/term-renderer.js +343 -0
- package/neural-interface/public/shared/ui-assistant-panel.js +10 -0
- package/neural-interface/public/shared/ui-assistant.js +77 -0
- package/neural-interface/public/shared/ui-automation-studio.js +5105 -3573
- package/neural-interface/public/shared/ui-claude-panel.js +11685 -6333
- package/neural-interface/public/shared/ui-codex-panel.js +12 -0
- package/neural-interface/public/shared/ui-cost-widget.js +55 -34
- package/neural-interface/public/shared/ui-detail.js +77 -14
- package/neural-interface/public/shared/ui-explorer.js +360 -181
- package/neural-interface/public/shared/ui-file-explorer.js +485 -107
- package/neural-interface/public/shared/ui-help.js +2 -2
- package/neural-interface/public/shared/ui-image-gallery.js +45 -21
- package/neural-interface/public/shared/ui-judgments.js +1346 -0
- package/neural-interface/public/shared/ui-keybinds.js +3 -2
- package/neural-interface/public/shared/ui-link.js +105 -51
- package/neural-interface/public/shared/ui-loading.js +4 -4
- package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
- package/neural-interface/public/shared/ui-menubar.js +89 -4
- package/neural-interface/public/shared/ui-native-loop-router-state.js +94 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +493 -0
- package/neural-interface/public/shared/ui-native-window-id.js +7 -0
- package/neural-interface/public/shared/ui-navbar.js +357 -222
- package/neural-interface/public/shared/ui-notifications.js +58 -12
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +17 -0
- package/neural-interface/public/shared/ui-panels.js +15 -7
- package/neural-interface/public/shared/ui-resume.js +823 -653
- package/neural-interface/public/shared/ui-schedule-queue.js +172 -0
- package/neural-interface/public/shared/ui-schedules-studio.js +1882 -0
- package/neural-interface/public/shared/ui-scroll-follow.js +194 -0
- package/neural-interface/public/shared/ui-sessions.js +789 -34
- package/neural-interface/public/shared/ui-settings.js +5793 -2190
- package/neural-interface/public/shared/ui-sidepanel-layout.js +109 -0
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +19 -0
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +40 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +221 -0
- package/neural-interface/public/shared/ui-sidepanel-state.js +131 -0
- package/neural-interface/public/shared/ui-sidepanel-tray.js +547 -0
- package/neural-interface/public/shared/ui-sidepanel-windows.js +387 -0
- package/neural-interface/public/shared/ui-skills.js +18 -32
- package/neural-interface/public/shared/ui-styleguide.js +35 -0
- package/neural-interface/public/shared/ui-sync.js +87 -0
- package/neural-interface/public/shared/ui-terminal.js +7877 -6385
- package/neural-interface/public/shared/ui-tool-updates.js +292 -0
- package/neural-interface/public/shared/ui-tooltip.js +156 -15
- package/neural-interface/public/shared/ui-tutorial-draw.js +119 -683
- package/neural-interface/public/shared/ui-tutorial-layout.js +135 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +59 -2209
- package/neural-interface/public/shared/ui-tutorial.css +102 -0
- package/neural-interface/public/shared/ui-tutorial.js +557 -1076
- package/neural-interface/public/shared/ui-update.js +420 -68
- package/neural-interface/public/shared/ui-whatsapp.js +941 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1076 -233
- package/neural-interface/public/shared/ui-workspaces.js +2 -1
- package/neural-interface/public/shared/utils.js +58 -0
- package/neural-interface/public/shared/vterm-buffer.js +15 -5
- package/neural-interface/public/shared/whatsapp/wa-phone.js +65 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +411 -0
- package/neural-interface/public/sidepanel-session.html +18 -0
- package/neural-interface/public/sw.js +8 -2
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/variant/2d/graph.js +1707 -1635
- package/neural-interface/public/variant/2d/lasso.js +2 -1
- package/neural-interface/public/variant/2d/layout.js +595 -594
- package/neural-interface/public/variant/2d/main.js +556 -521
- package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
- package/neural-interface/public/variant/3d/main.js +516 -500
- package/neural-interface/public/variant/3d/map-controls.js +144 -0
- package/neural-interface/public/variant/3d/map-data.js +251 -0
- package/neural-interface/public/variant/3d/map-labels.js +241 -0
- package/neural-interface/public/variant/3d/map-pick.js +90 -0
- package/neural-interface/public/variant/3d/map-renderer.js +521 -0
- package/neural-interface/public/variant/3d/map-shaders.js +158 -0
- package/neural-interface/public/variant/3d/map-view.js +618 -0
- package/neural-interface/public/vendor/three/manifest.json +3 -0
- package/neural-interface/public/vendor/three/three-0.169.0.min.js +3844 -0
- package/neural-interface/public/vendor/xterm/manifest.json +3 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.css +285 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.min.js +192 -0
- package/neural-interface/scripts/build-vendor.mjs +64 -0
- package/neural-interface/scripts/codex-protocol-fixture.mjs +83 -0
- package/neural-interface/scripts/rebuild-pty.js +37 -23
- package/neural-interface/scripts/vendor-three-entry.js +24 -0
- package/neural-interface/scripts/vendor-xterm-entry.js +8 -0
- package/neural-interface/server.js +33948 -15000
- package/neural-interface/skins/default/skin.css +90 -0
- package/neural-interface/skins/default/skin.json +8 -0
- package/neural-interface/templates/rulesets/README.md +9 -0
- package/neural-interface/templates/rulesets/claude.md +3 -0
- package/neural-interface/templates/rulesets/codex.md +2 -0
- package/neural-interface/templates/rulesets/coexistence.md +6 -0
- package/neural-interface/templates/rulesets/core.md +26 -0
- package/neural-interface/templates/rulesets/cursor.md +2 -0
- package/neural-interface/templates/rulesets/gemini.md +2 -0
- package/neural-interface/templates/rulesets/legacy-hashes.json +227 -0
- package/neural-interface/templates/rulesets/manifest.json +12 -0
- package/neural-interface/templates/rulesets/opencode.md +2 -0
- package/package.json +119 -53
- package/postinstall.js +125 -26
- package/preuninstall.js +187 -0
- package/scripts/audit-claude-hook-usage.mjs +12 -0
- package/scripts/audit-memory-duplicates.mjs +44 -0
- package/scripts/measure-claude-hook-overhead.mjs +77 -0
- package/scripts/recover-user-config.mjs +254 -0
- package/scripts/restore-data-home-from-backup.mjs +239 -0
- package/setup.js +804 -260
- package/skills/leonardo/SKILL.md +19 -10
- package/skills/leonardo/modules/image-prompter.md +18 -12
- package/skills/leonardo/modules/video-prompter.md +19 -13
- package/skills/synabun/SKILL.md +209 -185
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +380 -374
- package/skills/synabun/modules/changelog.md +15 -6
- package/skills/synabun/modules/idea.md +148 -144
- package/skills/synabun/modules/memorize.md +13 -4
- package/skills/synabun/modules/schedule.md +198 -0
- package/skills/synabun/synabunicon.png +0 -0
- package/updater.cmd +31 -0
- package/updater.mjs +382 -0
- package/updater.sh +29 -0
- package/hooks/claude-code/test-user-learning.mjs +0 -531
- package/neural-interface/public/claude-chat.css +0 -878
- package/neural-interface/public/claude-chat.html +0 -92
- package/neural-interface/public/claude-chat.js +0 -1168
- package/neural-interface/public/index-legacy.html +0 -14677
- package/neural-interface/public/index2d-legacy.html +0 -8193
- package/neural-interface/public/shared/html-term-renderer.js +0 -1022
- package/neural-interface/public/variant/3d/background.js +0 -884
- package/neural-interface/public/variant/3d/camera.js +0 -661
- package/neural-interface/public/variant/3d/gfx.js +0 -116
- package/neural-interface/public/variant/3d/graph.js +0 -2399
- package/neural-interface/public/variant/3d/settings-gfx.js +0 -228
- package/neural-interface/templates/CLAUDE-template.md +0 -211
|
@@ -0,0 +1,2009 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as ni from '../services/neural-interface.js';
|
|
3
|
+
import { getMemory } from '../services/sqlite.js';
|
|
4
|
+
import { cleanName, parseSeedQueue, regionForSeed, resolveClassification } from '../services/fb-regions.js';
|
|
5
|
+
import { text, image } from './response.js';
|
|
6
|
+
import * as assist from '../services/browser-assist.js';
|
|
7
|
+
import { coerceBoolean } from './utils.js';
|
|
8
|
+
const tabIdField = z.string().optional().describe('Target a specific tab within the session. Auto-resolved from environment if omitted.');
|
|
9
|
+
// ── browser_snapshot ──
|
|
10
|
+
export const browserSnapshotSchema = {
|
|
11
|
+
selector: z.string().optional().describe('Scope snapshot to a specific element\'s subtree. Dramatically reduces output on complex pages.'),
|
|
12
|
+
mode: z.enum(['ai', 'full', 'interactive', 'landmarks']).optional().describe('"ai" (default) = compact YAML with [ref=eN] element refs — pass ref to browser_click/fill/type to act on elements. "interactive" = flat list of clickable elements with selector hints. "full" = legacy accessibility tree. "landmarks" = structural overview.'),
|
|
13
|
+
depth: z.coerce.number().int().positive().optional().describe('Limit tree depth (mode="ai" only). Use 10-14 on deep feed pages to shrink output.'),
|
|
14
|
+
diff: coerceBoolean().optional().describe('Return only the changed region vs your previous snapshot of this tab (mode="ai" only).'),
|
|
15
|
+
baselineId: z.string().optional().describe('Diff against this delivered snapshot ID. A missing or incompatible baseline returns a full view.'),
|
|
16
|
+
force: coerceBoolean().optional().describe('Return the full snapshot even if the page is unchanged since your last snapshot.'),
|
|
17
|
+
viewport: coerceBoolean().optional().describe('Drop off-screen nodes (legacy modes only; ignored for mode="ai" — use depth/selector instead).'),
|
|
18
|
+
maxChars: z.coerce.number().int().positive().optional().describe('Cap snapshot text in characters, excluding brief metadata (default 30000). Truncates at complete lines.'),
|
|
19
|
+
intent: z.string().max(300).optional().describe('What you want to do on this page, in words (e.g. "open account settings"). mode="ai" only: the refs of THIS snapshot are ranked against it and the best one is named in a "Jev target:" line. Advisory — nothing is clicked, and no ref is invented.'),
|
|
20
|
+
format: z.enum(['text', 'json']).optional().describe('Only meaningful with mode="interactive": "json" = compact array.'),
|
|
21
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
22
|
+
tabId: tabIdField,
|
|
23
|
+
};
|
|
24
|
+
export const browserSnapshotDescription = 'Observe page structure and element refs for click/fill/type. Scope with selector on complex pages. AI mode deduplicates delivered observations and supports snapshot IDs and diffs. Refs target elements in the latest captured scope; refresh after navigation or a stale-ref error.';
|
|
25
|
+
// Format the server's mode="ai" snapshot payload (snapshotText/unchanged/diff fields)
|
|
26
|
+
// into the message body. Shared by snapshot, navigate, click, scroll.
|
|
27
|
+
export function formatAiSnapshotBody(result, maxChars) {
|
|
28
|
+
if (result.snapshotError)
|
|
29
|
+
return `(snapshot failed: ${result.snapshotError})`;
|
|
30
|
+
let body = result.unchanged ? '(observed scope unchanged)' : result.snapshotText
|
|
31
|
+
|| (result.snapshotTruncated ? '(no complete lines fit the budget)' : '(empty page)');
|
|
32
|
+
if (result.snapshotIsDiff) {
|
|
33
|
+
const pre = result.diffPrefixLines;
|
|
34
|
+
const suf = result.diffSuffixLines;
|
|
35
|
+
const ctx = (pre || suf) ? `; ${pre ?? 0} unchanged lines before, ${suf ?? 0} after` : '';
|
|
36
|
+
const signed = result.diffRemovedLines !== undefined ? '- removed, + added' : 'changed region';
|
|
37
|
+
body = `Diff (${signed}${ctx}):\n${body}`;
|
|
38
|
+
}
|
|
39
|
+
// New servers already cap complete delivered lines. Trimming again would hide
|
|
40
|
+
// lines that the server considers the caller's baseline and break later diffs.
|
|
41
|
+
let clientTruncated = false;
|
|
42
|
+
if (!result.snapshotBudgetApplied && body.length > maxChars) {
|
|
43
|
+
const end = body.lastIndexOf('\n', maxChars);
|
|
44
|
+
body = end < 0 ? '(no complete lines fit the budget)' : body.slice(0, end);
|
|
45
|
+
clientTruncated = true;
|
|
46
|
+
}
|
|
47
|
+
if (result.snapshotId) {
|
|
48
|
+
body = `Snapshot: ${result.snapshotId}${result.baselineId ? `; baseline: ${result.baselineId}` : ''}\n${body}`;
|
|
49
|
+
}
|
|
50
|
+
if (result.snapshotTruncated || clientTruncated)
|
|
51
|
+
body += '\n(truncated; narrow selector/depth or raise maxChars)';
|
|
52
|
+
if (result.aiDepth)
|
|
53
|
+
body += `\n(depth ${result.aiDepth}; set depth to override)`;
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
56
|
+
/** Origin only: the path and query of the page the agent is on never go into a judgment from here. */
|
|
57
|
+
function safeOrigin(url) {
|
|
58
|
+
try {
|
|
59
|
+
const u = new URL(String(url));
|
|
60
|
+
return u.protocol === 'http:' || u.protocol === 'https:' ? u.origin : '';
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Roles that are pure structural containers with no semantic value when unnamed
|
|
67
|
+
const NOISE_ROLES = new Set(['none', 'presentation', 'generic']);
|
|
68
|
+
const MAX_DEPTH = 20;
|
|
69
|
+
function formatSnapshotNode(node, indent = 0) {
|
|
70
|
+
if (!node || indent > MAX_DEPTH)
|
|
71
|
+
return '';
|
|
72
|
+
const role = node.role || 'generic';
|
|
73
|
+
const children = node.children;
|
|
74
|
+
const hasChildren = children && children.length > 0;
|
|
75
|
+
// Skip pure noise nodes: unnamed container roles with no value and no children
|
|
76
|
+
if (NOISE_ROLES.has(role) && !node.name && !node.value && !hasChildren) {
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
const prefix = ' '.repeat(indent);
|
|
80
|
+
const name = node.name ? ` "${node.name}"` : '';
|
|
81
|
+
const value = node.value ? ` value="${node.value}"` : '';
|
|
82
|
+
const desc = node.description ? ` (${node.description})` : '';
|
|
83
|
+
const checked = node.checked !== undefined ? ` [${node.checked ? 'checked' : 'unchecked'}]` : '';
|
|
84
|
+
const selected = node.selected ? ' [selected]' : '';
|
|
85
|
+
const expanded = node.expanded !== undefined ? ` [${node.expanded ? 'expanded' : 'collapsed'}]` : '';
|
|
86
|
+
const disabled = node.disabled ? ' [disabled]' : '';
|
|
87
|
+
const focused = node.focused ? ' [focused]' : '';
|
|
88
|
+
let line = `${prefix}${role}${name}${value}${desc}${checked}${selected}${expanded}${disabled}${focused}`;
|
|
89
|
+
if (hasChildren) {
|
|
90
|
+
const childLines = children
|
|
91
|
+
.map(c => formatSnapshotNode(c, indent + 1))
|
|
92
|
+
.filter(s => s.length > 0)
|
|
93
|
+
.join('\n');
|
|
94
|
+
if (childLines)
|
|
95
|
+
line += '\n' + childLines;
|
|
96
|
+
}
|
|
97
|
+
return line;
|
|
98
|
+
}
|
|
99
|
+
// Roles considered "interactive" for mode=interactive filtering.
|
|
100
|
+
const INTERACTIVE_ROLES = new Set([
|
|
101
|
+
'button', 'link', 'textbox', 'checkbox', 'combobox', 'menuitem', 'menuitemcheckbox',
|
|
102
|
+
'menuitemradio', 'tab', 'radio', 'slider', 'switch', 'searchbox', 'spinbutton', 'option',
|
|
103
|
+
]);
|
|
104
|
+
// Roles considered "landmarks" for mode=landmarks filtering.
|
|
105
|
+
const LANDMARK_ROLES = new Set([
|
|
106
|
+
'main', 'navigation', 'banner', 'contentinfo', 'complementary', 'region', 'form',
|
|
107
|
+
'search', 'heading',
|
|
108
|
+
]);
|
|
109
|
+
function filterTree(node, mode, keep) {
|
|
110
|
+
if (!node)
|
|
111
|
+
return null;
|
|
112
|
+
const role = node.role || 'generic';
|
|
113
|
+
const children = node.children || [];
|
|
114
|
+
const keptChildren = children
|
|
115
|
+
.map(c => filterTree(c, mode, keep))
|
|
116
|
+
.filter((c) => c !== null);
|
|
117
|
+
if (keep(role))
|
|
118
|
+
return { ...node, children: keptChildren };
|
|
119
|
+
// Hoist kept descendants through uninteresting parents
|
|
120
|
+
if (keptChildren.length > 0)
|
|
121
|
+
return { role: 'generic', name: '', children: keptChildren };
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Exported so navigate/click/scroll can format an attached returnSnapshot consistently.
|
|
125
|
+
export function formatInlineSnapshot(result, opts = {}) {
|
|
126
|
+
const mode = opts.mode || result.mode || 'full';
|
|
127
|
+
const format = opts.format || 'text';
|
|
128
|
+
const maxChars = opts.maxChars && opts.maxChars > 0 ? opts.maxChars : 12000;
|
|
129
|
+
if (mode === 'interactive') {
|
|
130
|
+
const flat = result.interactive || [];
|
|
131
|
+
if (format === 'json') {
|
|
132
|
+
const body = JSON.stringify(flat);
|
|
133
|
+
return body.length > maxChars ? body.slice(0, maxChars) + '\n... (truncated)' : body;
|
|
134
|
+
}
|
|
135
|
+
const lines = flat.map(h => {
|
|
136
|
+
const role = h.role || 'generic';
|
|
137
|
+
const label = h.ariaLabel || h.text || h.placeholder || '(unnamed)';
|
|
138
|
+
const sel = h.selector ? ` → ${h.selector}${h.nth !== undefined ? ` [nth:${h.nth}]` : ''}` : '';
|
|
139
|
+
return `- ${role}: "${String(label).slice(0, 60)}"${sel}`;
|
|
140
|
+
});
|
|
141
|
+
const body = lines.join('\n');
|
|
142
|
+
return body.length > maxChars ? body.slice(0, maxChars) + '\n... (truncated)' : body;
|
|
143
|
+
}
|
|
144
|
+
const tree = result.snapshot;
|
|
145
|
+
let working = tree;
|
|
146
|
+
if (tree && mode === 'landmarks') {
|
|
147
|
+
working = filterTree(tree, 'landmarks', r => LANDMARK_ROLES.has(r));
|
|
148
|
+
}
|
|
149
|
+
const formatted = working ? formatSnapshotNode(working) : '(empty page)';
|
|
150
|
+
return formatted.length > maxChars ? formatted.slice(0, maxChars) + '\n... (truncated)' : formatted;
|
|
151
|
+
}
|
|
152
|
+
function flattenInteractive(node) {
|
|
153
|
+
const out = [];
|
|
154
|
+
const role = node.role || '';
|
|
155
|
+
if (INTERACTIVE_ROLES.has(role)) {
|
|
156
|
+
const entry = { role, name: node.name || '' };
|
|
157
|
+
if (node.value)
|
|
158
|
+
entry.value = node.value;
|
|
159
|
+
if (node.description)
|
|
160
|
+
entry.description = node.description;
|
|
161
|
+
if (node.checked !== undefined)
|
|
162
|
+
entry.checked = node.checked;
|
|
163
|
+
if (node.selected)
|
|
164
|
+
entry.selected = true;
|
|
165
|
+
if (node.disabled)
|
|
166
|
+
entry.disabled = true;
|
|
167
|
+
out.push(entry);
|
|
168
|
+
}
|
|
169
|
+
const children = node.children || [];
|
|
170
|
+
for (const c of children)
|
|
171
|
+
out.push(...flattenInteractive(c));
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
export async function handleBrowserSnapshot(args) {
|
|
175
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
176
|
+
if ('error' in resolved)
|
|
177
|
+
return text(resolved.error);
|
|
178
|
+
const mode = args.mode || 'ai';
|
|
179
|
+
const intent = typeof args.intent === 'string' ? args.intent.trim() : '';
|
|
180
|
+
const rank = mode === 'ai' && intent.length > 0 && assist.assistAvailable('browser-target');
|
|
181
|
+
const format = args.format || 'text';
|
|
182
|
+
const maxChars = args.maxChars && args.maxChars > 0 ? args.maxChars : 30000;
|
|
183
|
+
const result = await ni.snapshot(resolved.sessionId, args.selector, resolved.tabId, {
|
|
184
|
+
mode,
|
|
185
|
+
viewport: args.viewport,
|
|
186
|
+
depth: args.depth,
|
|
187
|
+
diff: args.diff,
|
|
188
|
+
baselineId: args.baselineId,
|
|
189
|
+
force: args.force,
|
|
190
|
+
...(mode === 'ai' && { maxChars }),
|
|
191
|
+
// Ranking needs every ref of this capture even when only a diff or "unchanged" is shown.
|
|
192
|
+
...(rank && { includeFullText: true }),
|
|
193
|
+
});
|
|
194
|
+
if (result.error)
|
|
195
|
+
return text(`Snapshot failed: ${result.error}`);
|
|
196
|
+
if (mode === 'ai') {
|
|
197
|
+
let msg = `Page: ${result.url}\nTitle: "${result.title}"\n`;
|
|
198
|
+
if (args.selector)
|
|
199
|
+
msg += `Scope: ${args.selector}\n`;
|
|
200
|
+
msg += '\n' + formatAiSnapshotBody(result, maxChars);
|
|
201
|
+
if (rank) {
|
|
202
|
+
// The refs ranked are the ones this capture minted; a second capture would replace them.
|
|
203
|
+
const delivered = typeof result.snapshotFullText === 'string' ? result.snapshotFullText : typeof result.snapshotText === 'string' && !result.snapshotIsDiff ? result.snapshotText : '';
|
|
204
|
+
const line = delivered ? await assist.rankSnapshotRefs(delivered, intent, { origin: safeOrigin(result.url) }) : '';
|
|
205
|
+
if (line)
|
|
206
|
+
msg += `\n\n${line}`;
|
|
207
|
+
}
|
|
208
|
+
return text(msg);
|
|
209
|
+
}
|
|
210
|
+
const tree = result.snapshot;
|
|
211
|
+
// mode=interactive with format=json uses the server-provided flat list if present,
|
|
212
|
+
// otherwise derives it from the tree.
|
|
213
|
+
if (mode === 'interactive' && format === 'json') {
|
|
214
|
+
const flat = result.interactive
|
|
215
|
+
|| (tree ? flattenInteractive(tree) : []);
|
|
216
|
+
const body = JSON.stringify(flat);
|
|
217
|
+
let msg = `Page: ${result.url}\nTitle: "${result.title}"\nMode: interactive (json)\nCount: ${flat.length}\n\n${body}`;
|
|
218
|
+
if (msg.length > maxChars)
|
|
219
|
+
msg = msg.slice(0, maxChars) + '\n... (truncated — lower maxChars or use selector)';
|
|
220
|
+
return text(msg);
|
|
221
|
+
}
|
|
222
|
+
// mode=interactive with format=text: the server returns a flat `interactive` list
|
|
223
|
+
// (snapshot tree is null) — render it as one line per element with selector hints.
|
|
224
|
+
if (mode === 'interactive' && !tree && Array.isArray(result.interactive)) {
|
|
225
|
+
const flat = result.interactive;
|
|
226
|
+
const lines = flat.map(h => {
|
|
227
|
+
const label = h.ariaLabel || h.text || h.placeholder || '(unnamed)';
|
|
228
|
+
const sel = h.selector ? ` → ${h.selector}${h.nth !== undefined ? ` [nth:${h.nth}]` : ''}` : '';
|
|
229
|
+
return `- ${h.role || 'generic'}: "${String(label).slice(0, 60)}"${sel}`;
|
|
230
|
+
});
|
|
231
|
+
let msg = `Page: ${result.url}\nTitle: "${result.title}"\nMode: interactive\nCount: ${flat.length}\n\n${lines.join('\n') || '(no interactive elements)'}`;
|
|
232
|
+
if (msg.length > maxChars)
|
|
233
|
+
msg = msg.slice(0, maxChars) + '\n... (truncated — lower maxChars or use selector)';
|
|
234
|
+
return text(msg);
|
|
235
|
+
}
|
|
236
|
+
let working = tree;
|
|
237
|
+
if (tree && mode === 'interactive') {
|
|
238
|
+
working = filterTree(tree, 'interactive', r => INTERACTIVE_ROLES.has(r));
|
|
239
|
+
}
|
|
240
|
+
else if (tree && mode === 'landmarks') {
|
|
241
|
+
working = filterTree(tree, 'landmarks', r => LANDMARK_ROLES.has(r));
|
|
242
|
+
}
|
|
243
|
+
const formatted = working ? formatSnapshotNode(working) : '(empty page)';
|
|
244
|
+
let msg = `Page: ${result.url}\nTitle: "${result.title}"\n`;
|
|
245
|
+
if (args.selector)
|
|
246
|
+
msg += `Scope: ${args.selector}\n`;
|
|
247
|
+
if (mode !== 'full')
|
|
248
|
+
msg += `Mode: ${mode}\n`;
|
|
249
|
+
if (args.viewport)
|
|
250
|
+
msg += `Viewport-filtered\n`;
|
|
251
|
+
msg += '\n' + formatted;
|
|
252
|
+
if (msg.length > maxChars) {
|
|
253
|
+
msg = msg.slice(0, maxChars) + '\n... (truncated — narrow scope, lower maxChars, or use mode="interactive")';
|
|
254
|
+
}
|
|
255
|
+
return text(msg);
|
|
256
|
+
}
|
|
257
|
+
// ── browser_content ──
|
|
258
|
+
export const browserContentSchema = {
|
|
259
|
+
selector: z.string().optional().describe('Read one article or result container instead of the whole page. Must match exactly one element.'),
|
|
260
|
+
format: z.enum(['text', 'markdown']).optional().default('text').describe('Output format. "text" = raw innerText. "markdown" = clean markdown (headings/links/lists, nav/footer/ads stripped) — better for LLM consumption.'),
|
|
261
|
+
maxChars: z.coerce.number().int().positive().optional().describe('Max characters returned (default 20000). Response notes total size when truncated.'),
|
|
262
|
+
offset: z.coerce.number().int().nonnegative().optional().describe('Character offset to continue a previously truncated read.'),
|
|
263
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
264
|
+
tabId: tabIdField,
|
|
265
|
+
};
|
|
266
|
+
export const browserContentDescription = 'Get the content of the current page. Set format="markdown" for clean markdown with structure preserved — preferred for LLM consumption. Returns up to maxChars (default 20000); pass offset to page through long pages.';
|
|
267
|
+
const DEFAULT_CONTENT_MAX_CHARS = 20000;
|
|
268
|
+
export async function handleBrowserContent(args) {
|
|
269
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
270
|
+
if ('error' in resolved)
|
|
271
|
+
return text(resolved.error);
|
|
272
|
+
const pageOpts = {
|
|
273
|
+
selector: args.selector,
|
|
274
|
+
maxChars: args.maxChars && args.maxChars > 0 ? args.maxChars : DEFAULT_CONTENT_MAX_CHARS,
|
|
275
|
+
offset: args.offset && args.offset > 0 ? args.offset : undefined,
|
|
276
|
+
};
|
|
277
|
+
if (args.format === 'markdown') {
|
|
278
|
+
const result = await ni.getMarkdown(resolved.sessionId, resolved.tabId, pageOpts);
|
|
279
|
+
if (result.error)
|
|
280
|
+
return text(`Markdown extraction failed: ${result.error}`);
|
|
281
|
+
let msg = `URL: ${result.url}\nTitle: "${result.title}"\nTokens: ~${result.tokens}\n\n`;
|
|
282
|
+
msg += result.markdown || '(empty page)';
|
|
283
|
+
return text(msg);
|
|
284
|
+
}
|
|
285
|
+
// Default: plain text
|
|
286
|
+
const result = await ni.getContent(resolved.sessionId, resolved.tabId, pageOpts);
|
|
287
|
+
if (result.error)
|
|
288
|
+
return text(`Content failed: ${result.error}`);
|
|
289
|
+
let msg = `URL: ${result.url}\nTitle: "${result.title}"\n\n`;
|
|
290
|
+
msg += result.text || '(empty page)';
|
|
291
|
+
return text(msg);
|
|
292
|
+
}
|
|
293
|
+
// ── browser_screenshot ──
|
|
294
|
+
export const browserScreenshotSchema = {
|
|
295
|
+
width: z.coerce.number().int().min(100).max(4096).optional().describe('Viewport width in CSS px for this capture only, e.g. 375, 768 and 1440 for breakpoints. Emulated on the tab (mobile below 768) and reset right after.'),
|
|
296
|
+
height: z.coerce.number().int().min(100).max(4096).optional().describe('Viewport height in CSS px for this capture only (default: the current height).'),
|
|
297
|
+
fullPage: coerceBoolean().optional().describe('Capture the whole scrollable page instead of the viewport.'),
|
|
298
|
+
format: z.enum(['png', 'jpeg']).optional().describe('Image format. Default: JPEG for the returned image, PNG for a saved file.'),
|
|
299
|
+
save: coerceBoolean().optional().describe('Also save the full-resolution image as a file (default folder ~/.synabun/data/media/screenshots/<date>/) and return its path.'),
|
|
300
|
+
path: z.string().optional().describe('Absolute .png / .jpg / .jpeg file to save to, inside a registered project or the screenshots folder (implies save). Parent folders are created; only an existing image is ever replaced.'),
|
|
301
|
+
maxWidth: z.coerce.number().int().min(0).optional().describe('Downscale the returned image to this width in px (default 1024). 0 = native resolution. A saved file is always full resolution.'),
|
|
302
|
+
quality: z.coerce.number().int().min(10).max(100).optional().describe('JPEG quality 10-100 (default 60 for the returned image, 90 for a saved JPEG).'),
|
|
303
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
304
|
+
tabId: tabIdField,
|
|
305
|
+
};
|
|
306
|
+
export const browserScreenshotDescription = 'Take a screenshot of the current page (localhost pages included): the viewport as JPEG, downscaled to 1024 px wide, by default. ' +
|
|
307
|
+
'For visual QA pass width/height (breakpoints 375 / 768 / 1440 — emulated for this capture only), fullPage for the whole page, and save or path to write a PNG and get its path. ' +
|
|
308
|
+
'Prefer browser_snapshot to read structure; it is far more token-efficient.';
|
|
309
|
+
export async function handleBrowserScreenshot(args) {
|
|
310
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
311
|
+
if ('error' in resolved)
|
|
312
|
+
return text(resolved.error);
|
|
313
|
+
const result = await ni.screenshot(resolved.sessionId, resolved.tabId, {
|
|
314
|
+
maxWidth: args.maxWidth,
|
|
315
|
+
quality: args.quality,
|
|
316
|
+
width: args.width,
|
|
317
|
+
height: args.height,
|
|
318
|
+
fullPage: args.fullPage,
|
|
319
|
+
format: args.format,
|
|
320
|
+
save: args.save,
|
|
321
|
+
path: args.path,
|
|
322
|
+
});
|
|
323
|
+
if (result.error)
|
|
324
|
+
return text(`Screenshot failed: ${result.error}`);
|
|
325
|
+
const details = [];
|
|
326
|
+
if (typeof result.width === 'number' && typeof result.height === 'number')
|
|
327
|
+
details.push(`${result.width}×${result.height}`);
|
|
328
|
+
const viewport = result.viewport;
|
|
329
|
+
if (viewport?.width)
|
|
330
|
+
details.push(`viewport ${viewport.width}×${viewport.height}`);
|
|
331
|
+
if (result.fullPage)
|
|
332
|
+
details.push(result.truncated ? 'full page, cut at the capture limit' : 'full page');
|
|
333
|
+
let msg = `Screenshot of ${result.url} — "${result.title}"${details.length ? ` (${details.join(', ')})` : ''}`;
|
|
334
|
+
if (typeof result.savedPath === 'string')
|
|
335
|
+
msg += `\nSaved: ${result.savedPath}`;
|
|
336
|
+
// The tab was closed (its viewport was not confirmed restored after the capture).
|
|
337
|
+
if (typeof result.notice === 'string')
|
|
338
|
+
msg += `\n${result.notice}`;
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
...text(msg).content,
|
|
342
|
+
...image(result.data, typeof result.mime === 'string' ? result.mime : 'image/jpeg').content,
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// ── browser_console ──
|
|
347
|
+
export const browserConsoleSchema = {
|
|
348
|
+
level: z.enum(['all', 'error', 'warning', 'info', 'log']).optional().describe('Lowest level to return; each level includes the more severe ones (error < warning < info < log). Default "all". Uncaught page errors count as errors.'),
|
|
349
|
+
since: z.coerce.string().optional().describe('Only entries after this time (ISO timestamp or epoch ms), e.g. the "latest" value of a previous call.'),
|
|
350
|
+
limit: z.coerce.number().int().min(1).max(200).optional().describe('Return at most this many of the newest matching entries (default 50).'),
|
|
351
|
+
clear: coerceBoolean().optional().describe('Empty the tab\'s buffer after reading, so the next call shows only new entries.'),
|
|
352
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
353
|
+
tabId: tabIdField,
|
|
354
|
+
};
|
|
355
|
+
export const browserConsoleDescription = 'Read the current tab\'s console messages and uncaught page errors (the last 200, kept from when SynaBun opened the tab), localhost pages included. ' +
|
|
356
|
+
'Use level "error" to check a page for errors, and clear or since to see only what a reload or an action produced.';
|
|
357
|
+
function formatConsoleEntry(entry) {
|
|
358
|
+
const time = typeof entry.at === 'string' ? entry.at.slice(11, 23) : '';
|
|
359
|
+
// A popup this tab opened (popups are not tabs of their own here).
|
|
360
|
+
const popup = typeof entry.popup === 'string' ? `[popup ${entry.popup}] ` : '';
|
|
361
|
+
if (entry.kind === 'pageerror') {
|
|
362
|
+
const stack = entry.stack && entry.stack !== entry.message ? `\n ${entry.stack.split('\n').slice(1, 6).map(line => line.trim()).join('\n ')}` : '';
|
|
363
|
+
return `[${time}] ${popup}page error ${entry.message || ''}${stack}`;
|
|
364
|
+
}
|
|
365
|
+
const where = entry.location ? ` (${entry.location})` : '';
|
|
366
|
+
return `[${time}] ${popup}${entry.level || 'log'} ${entry.text || ''}${where}`;
|
|
367
|
+
}
|
|
368
|
+
export async function handleBrowserConsole(args) {
|
|
369
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
370
|
+
if ('error' in resolved)
|
|
371
|
+
return text(resolved.error);
|
|
372
|
+
const result = await ni.consoleMessages(resolved.sessionId, resolved.tabId, {
|
|
373
|
+
level: args.level,
|
|
374
|
+
since: args.since,
|
|
375
|
+
limit: args.limit,
|
|
376
|
+
clear: args.clear,
|
|
377
|
+
});
|
|
378
|
+
if (result.error)
|
|
379
|
+
return text(`Console read failed: ${result.error}`);
|
|
380
|
+
const entries = (Array.isArray(result.entries) ? result.entries : []);
|
|
381
|
+
const level = args.level && args.level !== 'all' ? ` at level ${args.level} or above` : '';
|
|
382
|
+
let msg = `Console of ${result.url} — "${result.title}"\n`;
|
|
383
|
+
if (result.attached === false) {
|
|
384
|
+
msg += 'SynaBun was not recording this tab yet; it records from now on. Reload the page and call browser_console again.';
|
|
385
|
+
return text(msg);
|
|
386
|
+
}
|
|
387
|
+
const matched = typeof result.matched === 'number' ? result.matched : entries.length;
|
|
388
|
+
msg += entries.length
|
|
389
|
+
? `${entries.length === matched ? matched : `${entries.length} newest of ${matched}`} entr${matched === 1 ? 'y' : 'ies'}${level}:\n${entries.map(formatConsoleEntry).join('\n')}`
|
|
390
|
+
: `No entries${level}${args.since ? ` after ${args.since}` : ''}.`;
|
|
391
|
+
if (typeof result.dropped === 'number' && result.dropped > 0)
|
|
392
|
+
msg += `\n(${result.dropped} older entries were dropped: the buffer keeps the last 200.)`;
|
|
393
|
+
if (typeof result.latest === 'string')
|
|
394
|
+
msg += `\nlatest: ${result.latest}`;
|
|
395
|
+
if (result.cleared)
|
|
396
|
+
msg += '\nThe buffer was cleared.';
|
|
397
|
+
return text(msg);
|
|
398
|
+
}
|
|
399
|
+
// ── Paged extraction (scroll → extract → merge → dedupe in ONE tool call) ──
|
|
400
|
+
// V2 performs the full cycle in one server operation, avoiding per-round HTTP
|
|
401
|
+
// requests and returning only the requested data to the model.
|
|
402
|
+
const pagedExtractorFields = {
|
|
403
|
+
scrolls: z.coerce.number().int().min(0).max(10).optional().describe('Auto-scroll up to N times, extracting + deduping after each (default 0 = current view only). Stops early at minItems or end of feed.'),
|
|
404
|
+
minItems: z.coerce.number().int().positive().optional().describe('Stop scrolling once this many unique items are collected.'),
|
|
405
|
+
maxItems: z.coerce.number().int().positive().optional().describe('Cap on returned items (default 50).'),
|
|
406
|
+
fields: z.array(z.string().min(1)).min(1).optional().describe('Return only these fields; identity deduplication still uses the full items.'),
|
|
407
|
+
maxChars: z.coerce.number().int().min(2).optional().describe('Cap the JSON items in characters, keeping complete items. Defaults to no additional character cap.'),
|
|
408
|
+
timeoutMs: z.coerce.number().int().positive().max(30000).optional().describe('Total extraction deadline with browser V2 enabled (default 15000ms).'),
|
|
409
|
+
};
|
|
410
|
+
async function runPagedExtractor(sessionId, tabId, script, opts) {
|
|
411
|
+
if (ni.isBrowserV2Enabled()) {
|
|
412
|
+
const result = await ni.extract(sessionId, script, tabId, opts);
|
|
413
|
+
if (result.error)
|
|
414
|
+
return { error: result.error };
|
|
415
|
+
const res = result;
|
|
416
|
+
if (!Array.isArray(res.items))
|
|
417
|
+
return { error: 'Invalid extraction response: items are missing.' };
|
|
418
|
+
if (res.items.length === 0 && res.budgetReason === 'max_chars')
|
|
419
|
+
return { error: 'maxChars is too small for the first complete item. Increase maxChars or select fewer fields.' };
|
|
420
|
+
return res;
|
|
421
|
+
}
|
|
422
|
+
const scrolls = Math.min(Math.max(opts.scrolls ?? 0, 0), 10);
|
|
423
|
+
const maxItems = opts.maxItems && opts.maxItems > 0 ? opts.maxItems : 50;
|
|
424
|
+
const distance = (opts.scrollDistance ?? 1200) * (opts.scrollDirection ?? 1);
|
|
425
|
+
const settleMs = opts.settleMs ?? 900;
|
|
426
|
+
const itemKey = (item) => {
|
|
427
|
+
for (const k of opts.dedupeKeys) {
|
|
428
|
+
const v = item[k];
|
|
429
|
+
if (v)
|
|
430
|
+
return `${k}:${v}`;
|
|
431
|
+
}
|
|
432
|
+
return JSON.stringify(item);
|
|
433
|
+
};
|
|
434
|
+
const collected = new Map();
|
|
435
|
+
let raw = 0;
|
|
436
|
+
let scrollsUsed = 0;
|
|
437
|
+
let dryRounds = 0;
|
|
438
|
+
let bonusRounds = 0;
|
|
439
|
+
let failureReason;
|
|
440
|
+
let stopReason = 'scroll_limit';
|
|
441
|
+
for (let round = 0;; round++) {
|
|
442
|
+
const result = await ni.evaluate(sessionId, script, tabId);
|
|
443
|
+
if (result.error) {
|
|
444
|
+
if (collected.size > 0) {
|
|
445
|
+
failureReason = result.error;
|
|
446
|
+
stopReason = 'extraction_error';
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
return { error: result.error };
|
|
450
|
+
}
|
|
451
|
+
const items = Array.isArray(result.result) ? result.result : [];
|
|
452
|
+
raw += items.length;
|
|
453
|
+
const before = collected.size;
|
|
454
|
+
for (const item of items) {
|
|
455
|
+
const key = itemKey(item);
|
|
456
|
+
if (!collected.has(key))
|
|
457
|
+
collected.set(key, item);
|
|
458
|
+
}
|
|
459
|
+
if (opts.minItems && collected.size >= opts.minItems) {
|
|
460
|
+
stopReason = 'min_items';
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
if (collected.size >= maxItems) {
|
|
464
|
+
stopReason = 'max_items';
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
// Some feeds (Facebook groups/Pages) virtualize content in only after a scroll, so
|
|
468
|
+
// a fresh page can extract empty. Grant one bonus scroll round in that case.
|
|
469
|
+
if (opts.scrollIfEmpty && round === 0 && collected.size === 0)
|
|
470
|
+
bonusRounds = 1;
|
|
471
|
+
if (round >= scrolls + bonusRounds)
|
|
472
|
+
break;
|
|
473
|
+
dryRounds = collected.size > before ? 0 : dryRounds + 1;
|
|
474
|
+
if (dryRounds >= 2) {
|
|
475
|
+
stopReason = 'end_of_feed';
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
const scrollExpr = opts.scrollTarget
|
|
479
|
+
? `(() => { const el = ${opts.scrollTarget}; if (el) el.scrollBy(0, ${distance}); return true; })()`
|
|
480
|
+
: `window.scrollBy(0, ${distance}); true`;
|
|
481
|
+
const scrolled = await ni.evaluate(sessionId, scrollExpr, tabId);
|
|
482
|
+
if (scrolled.error) {
|
|
483
|
+
if (!collected.size)
|
|
484
|
+
return { error: scrolled.error };
|
|
485
|
+
failureReason = scrolled.error;
|
|
486
|
+
stopReason = 'extraction_error';
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
scrollsUsed++;
|
|
490
|
+
// Jitter the settle so the scroll cadence is not a fixed, fingerprintable
|
|
491
|
+
// interval (~0.8x–1.5x of base). Floor keeps slow feeds readable.
|
|
492
|
+
const jittered = Math.round(settleMs * (0.8 + Math.random() * 0.7));
|
|
493
|
+
await new Promise(r => setTimeout(r, Math.max(250, jittered)));
|
|
494
|
+
}
|
|
495
|
+
const all = [...collected.values()];
|
|
496
|
+
const items = [];
|
|
497
|
+
let chars = 2;
|
|
498
|
+
let budgetReason;
|
|
499
|
+
for (const item of all) {
|
|
500
|
+
if (items.length >= maxItems) {
|
|
501
|
+
budgetReason = 'max_items';
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
const value = opts.fields ? Object.fromEntries(opts.fields.filter(key => Object.hasOwn(item, key)).map(key => [key, item[key]])) : item;
|
|
505
|
+
const size = JSON.stringify(value).length + (items.length ? 1 : 0);
|
|
506
|
+
if (opts.maxChars !== undefined && chars + size > opts.maxChars) {
|
|
507
|
+
budgetReason = 'max_chars';
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
items.push(value);
|
|
511
|
+
chars += size;
|
|
512
|
+
}
|
|
513
|
+
if (items.length === 0 && budgetReason === 'max_chars')
|
|
514
|
+
return { error: 'maxChars is too small for the first complete item. Increase maxChars or select fewer fields.' };
|
|
515
|
+
return { items, raw, scrollsUsed, truncated: items.length < all.length, partial: !!failureReason, stopReason: budgetReason || stopReason, failureReason, budgetReason };
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* An extractor found nothing. Its own message stays the first line, word for word; when a
|
|
519
|
+
* page-state judgment is available it adds why — still loading, a sign-in or verification wall,
|
|
520
|
+
* a consent dialog, a rendered error, the wrong surface, or a page that really is empty.
|
|
521
|
+
* Populated results never come through here, and the maxChars budget error returns before it.
|
|
522
|
+
*/
|
|
523
|
+
async function emptyExtraction(resolved, tool, expectedSurface, message) {
|
|
524
|
+
const diagnosis = await assist.diagnoseEmptyExtraction(resolved.sessionId, resolved.tabId, { tool, expectedSurface }).catch(() => '');
|
|
525
|
+
return text(diagnosis ? `${message}\n${diagnosis}` : message);
|
|
526
|
+
}
|
|
527
|
+
// Compact single-line JSON: pretty-printing buys a model nothing and costs ~25-35%.
|
|
528
|
+
function formatExtraction(noun, res) {
|
|
529
|
+
const extras = [];
|
|
530
|
+
if (res.raw > res.items.length)
|
|
531
|
+
extras.push(`${res.truncated ? 'selected' : 'deduped'} from ${res.raw} raw`);
|
|
532
|
+
if (res.scrollsUsed > 0)
|
|
533
|
+
extras.push(`${res.scrollsUsed} scroll(s)`);
|
|
534
|
+
if (res.stopReason)
|
|
535
|
+
extras.push(`stopped: ${res.stopReason}`);
|
|
536
|
+
let msg = `${res.items.length} ${noun}(s)${extras.length ? ` (${extras.join(', ')})` : ''}:\n\n${JSON.stringify(res.items)}`;
|
|
537
|
+
if (res.truncated)
|
|
538
|
+
msg += `\n\n(capped at ${res.budgetReason === 'max_chars' ? 'maxChars — increase it or select fewer fields' : 'maxItems — increase it to return more items'}; complete items preserved)`;
|
|
539
|
+
if (res.partial)
|
|
540
|
+
msg += `\n\nPartial results: ${res.failureReason || 'extraction ended before completion'}`;
|
|
541
|
+
return msg;
|
|
542
|
+
}
|
|
543
|
+
// Finds the nearest scrollable ancestor of a node — used for panes without stable
|
|
544
|
+
// container selectors (WhatsApp message history).
|
|
545
|
+
const SCROLLABLE_ANCESTOR = (innerSelector) => `(() => { let el = document.querySelector('${innerSelector}'); while (el && el.scrollHeight <= el.clientHeight + 10) el = el.parentElement; return el; })()`;
|
|
546
|
+
// ── browser_extract_tweets ──
|
|
547
|
+
const TWEET_EXTRACTOR_SCRIPT = `
|
|
548
|
+
Array.from(document.querySelectorAll('[data-testid="tweet"]')).map(el => {
|
|
549
|
+
const userNameEl = el.querySelector('[data-testid="User-Name"]');
|
|
550
|
+
const lines = userNameEl ? userNameEl.innerText.split('\\n').filter(Boolean) : [];
|
|
551
|
+
const statusLink = el.querySelector('a[href*="/status/"]');
|
|
552
|
+
return {
|
|
553
|
+
author: lines[0] || null,
|
|
554
|
+
handle: lines.find(l => l.startsWith('@')) || null,
|
|
555
|
+
text: el.querySelector('[data-testid="tweetText"]')?.innerText || null,
|
|
556
|
+
time: el.querySelector('time')?.getAttribute('datetime') || null,
|
|
557
|
+
url: statusLink ? statusLink.href : null,
|
|
558
|
+
replies: el.querySelector('[data-testid="reply"] span[data-testid="app-text-transition-container"]')?.innerText || null,
|
|
559
|
+
reposts: el.querySelector('[data-testid="retweet"] span[data-testid="app-text-transition-container"]')?.innerText || null,
|
|
560
|
+
likes: el.querySelector('[data-testid="like"] span[data-testid="app-text-transition-container"]')?.innerText || null,
|
|
561
|
+
views: el.querySelector('a[href*="/analytics"] span')?.innerText || null,
|
|
562
|
+
};
|
|
563
|
+
})
|
|
564
|
+
`.trim();
|
|
565
|
+
export const browserExtractTweetsSchema = {
|
|
566
|
+
...pagedExtractorFields,
|
|
567
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
568
|
+
tabId: tabIdField,
|
|
569
|
+
};
|
|
570
|
+
export const browserExtractTweetsDescription = 'Extract visible tweets as structured JSON (author, handle, text, time, url, replies, reposts, likes, views). Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call (e.g. scrolls:3 minItems:30). Much faster than browser_snapshot for data harvesting. Navigate to x.com/search?q=%23hashtag&f=live first for latest-first hashtag results.';
|
|
571
|
+
export async function handleBrowserExtractTweets(args) {
|
|
572
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
573
|
+
if ('error' in resolved)
|
|
574
|
+
return text(resolved.error);
|
|
575
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, TWEET_EXTRACTOR_SCRIPT, {
|
|
576
|
+
...args, dedupeKeys: ['url'],
|
|
577
|
+
});
|
|
578
|
+
if ('error' in res)
|
|
579
|
+
return text(`Extract failed: ${res.error}`);
|
|
580
|
+
if (res.items.length === 0)
|
|
581
|
+
return emptyExtraction(resolved, 'browser_extract_tweets', 'an X/Twitter timeline, profile or search results page', 'No tweets found. Try browser_scroll (or pass scrolls:2) then retry.');
|
|
582
|
+
return text(formatExtraction('tweet', res));
|
|
583
|
+
}
|
|
584
|
+
// ── browser_x_compose_state ──
|
|
585
|
+
// Read-only structured read of the X composer so a loop branches on fields instead of
|
|
586
|
+
// eyeballing a screenshot. The detection script + the loop publish gate that shares it
|
|
587
|
+
// live server-side (neural-interface/server.js); this tool just forwards to that endpoint.
|
|
588
|
+
export const browserXComposeStateSchema = {
|
|
589
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
590
|
+
tabId: tabIdField,
|
|
591
|
+
};
|
|
592
|
+
export const browserXComposeStateDescription = 'Inspect the X/Twitter compose surface in ONE call (read-only). Returns: composerOpen, composerText (read back from [data-testid="tweetTextarea_0"]), charCount, overLimit; ' +
|
|
593
|
+
'quoteCard {present, type:"quote"|"link"|"none", author, handle, text, url} — type "quote" means a real embedded quoted-tweet card rendered (safe to post a quote), "link" means only a plain link-preview card (posting would publish a bare reply — do NOT post; fix the status URL or skip the target), "none" means nothing embedded; ' +
|
|
594
|
+
'submitButton {testid:"tweetButtonInline"|"tweetButton", present, enabled, selector} (tweetButtonInline for inline replies, tweetButton for the compose dialog); ' +
|
|
595
|
+
'isModal (the glitchy centered reply dialog is open — close it and retry inline), composerCount (>1 ⇒ pass nthMatch to disambiguate the two reply textareas), hasStaleDraft (duplicated/garbled leftover text — discard and reopen), warnings[]; and ' +
|
|
596
|
+
'recommendedAction {submit|type|open-composer|fix-quote-url|trim-text|unknown} — the deterministic next step. ' +
|
|
597
|
+
'QUOTE flow: navigate x.com/compose/tweet, browser_type the full status URL, call this probe, post via submitButton.selector ONLY if quoteCard.type==="quote". ' +
|
|
598
|
+
'REPLY flow: click the reply icon, call this probe, if isModal close it and retry inline, type, post via submitButton.selector. ' +
|
|
599
|
+
'After submitting, re-call this probe (composer should have cleared) and confirm with browser_extract_tweets on the author profile /with_replies (a NEW, higher status id = published). ' +
|
|
600
|
+
'On loop tabs a server-side gate ALSO blocks a tweetButton click when a status URL is in the body but quoteCard.type!=="quote" (response carries quoteGuard) — so a quote can never silently misfire into a bare reply. Read-only — never types or clicks. ' +
|
|
601
|
+
'When recommendedAction is "unknown" or warnings[] is non-empty, an additive `judgment` {advisory:true, state, confidence, suggestedControl?, note} may appear: a Jev reading of the ambiguous probe built from booleans and counts only (never the composer text or the quoted post). It never changes the fields above; re-probe after acting on it.';
|
|
602
|
+
export async function handleBrowserXComposeState(args) {
|
|
603
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
604
|
+
if ('error' in resolved)
|
|
605
|
+
return text(resolved.error);
|
|
606
|
+
const result = await ni.xComposeState(resolved.sessionId, resolved.tabId);
|
|
607
|
+
if (result.error)
|
|
608
|
+
return text(`X compose state probe failed: ${result.error}`);
|
|
609
|
+
// Only when the probe could not decide. Jev sees booleans, counts and button names — never composerText or the quoted post.
|
|
610
|
+
const judgment = await assist.judgeComposer('x', result.result, resolved.sessionId, resolved.tabId).catch(() => null);
|
|
611
|
+
return text(JSON.stringify(judgment && result.result && typeof result.result === 'object' ? { ...result.result, judgment } : result.result));
|
|
612
|
+
}
|
|
613
|
+
// ── browser_extract_fb_posts ──
|
|
614
|
+
const FB_POST_EXTRACTOR_SCRIPT = `
|
|
615
|
+
Array.from(document.querySelectorAll('[role="article"]')).map(el => {
|
|
616
|
+
const h2Link = el.querySelector('h2 a, h3 a, h4 a, strong a');
|
|
617
|
+
const timeLink = el.querySelector('a[href*="?__cft__"], a[href*="/posts/"], a[href*="/permalink/"]');
|
|
618
|
+
const abbr = el.querySelector('abbr[title], abbr[data-utime]');
|
|
619
|
+
const textEl = el.querySelector('[data-ad-comet-preview="message"], [data-ad-preview="message"]');
|
|
620
|
+
const reactEl = el.querySelector('[aria-label*="reaction"], [aria-label*="reação"], [aria-label*="tepki"]');
|
|
621
|
+
return {
|
|
622
|
+
author: h2Link ? h2Link.textContent.trim() : null,
|
|
623
|
+
authorUrl: h2Link ? h2Link.href : null,
|
|
624
|
+
text: textEl ? textEl.innerText.trim() : (el.querySelector('[dir="auto"]')?.innerText?.trim() || null),
|
|
625
|
+
time: abbr ? (abbr.getAttribute('title') || abbr.textContent.trim()) : (timeLink ? timeLink.textContent.trim() : null),
|
|
626
|
+
postUrl: timeLink ? timeLink.href : null,
|
|
627
|
+
reactions: reactEl ? reactEl.getAttribute('aria-label') : null,
|
|
628
|
+
};
|
|
629
|
+
}).filter(p => p.author || p.text)
|
|
630
|
+
`.trim();
|
|
631
|
+
export const browserExtractFbPostsSchema = {
|
|
632
|
+
...pagedExtractorFields,
|
|
633
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
634
|
+
tabId: tabIdField,
|
|
635
|
+
};
|
|
636
|
+
export const browserExtractFbPostsDescription = 'Extract visible Facebook posts as structured JSON (author, authorUrl, text, time, postUrl, reactions). ' +
|
|
637
|
+
'Works on group feeds and Pages. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. ' +
|
|
638
|
+
'Much faster than browser_snapshot for data harvesting from Facebook.';
|
|
639
|
+
export async function handleBrowserExtractFbPosts(args) {
|
|
640
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
641
|
+
if ('error' in resolved)
|
|
642
|
+
return text(resolved.error);
|
|
643
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, FB_POST_EXTRACTOR_SCRIPT, {
|
|
644
|
+
...args, dedupeKeys: ['postUrl'], scrollIfEmpty: true,
|
|
645
|
+
});
|
|
646
|
+
if ('error' in res)
|
|
647
|
+
return text(`Extract failed: ${res.error}`);
|
|
648
|
+
if (res.items.length === 0) {
|
|
649
|
+
return emptyExtraction(resolved, 'browser_extract_fb_posts', 'a Facebook feed, group or Page with posts', 'No posts found even after an auto-scroll. The feed may still be loading — retry with scrolls:3.');
|
|
650
|
+
}
|
|
651
|
+
return text(formatExtraction('post', res));
|
|
652
|
+
}
|
|
653
|
+
// ── browser_fb_composer_state ──
|
|
654
|
+
// Single locale-tolerant probe that replaces the ad-hoc inline browser_evaluate recon
|
|
655
|
+
// every Facebook posting loop used to re-send each iteration. Returns modal state, the
|
|
656
|
+
// real (non-search/non-stale) composer candidates scoped to inline vs dialog, the submit
|
|
657
|
+
// button scoped to the active composer, and a coarse submission state for verification.
|
|
658
|
+
// Prefers role + aria + visibility over translated UI strings (account UI may be PT even
|
|
659
|
+
// inside EN/FR/ES groups). Read-only: it inspects, it never types or clicks.
|
|
660
|
+
const FB_COMPOSER_STATE_SCRIPT = `
|
|
661
|
+
(() => {
|
|
662
|
+
// Submit-button labels. PT-BR group composer uses "Postar" (NOT "Publicar"); German
|
|
663
|
+
// approval groups use "Senden"/"Veröffentlichen"/"Zur Genehmigung senden" — those gaps are
|
|
664
|
+
// why the probe used to return submitButton:null and the agent stalled on the modal.
|
|
665
|
+
const PUB = ['publicar','postar','post','paylaş','publier','posten','pubblica','opublikuj','publiceren','plaatsen','veröffentlichen','senden','an gruppe senden','zur genehmigung senden','zur bestätigung senden'];
|
|
666
|
+
// Buttons that may CONTAIN a publish token but are NOT the submit action ("Add to your
|
|
667
|
+
// post" / "Zu deinem Beitrag hinzufügen" / "Report post"). Used to guard substring matching.
|
|
668
|
+
// Includes paid-promotion verbs (boost/turbinar/impulsionar/promote) so the submit
|
|
669
|
+
// detection + footer-button heuristic can NEVER select Facebook's "Boost post" /
|
|
670
|
+
// "Turbinar publicação" button as the post action. SynaBun posts organically only.
|
|
671
|
+
const NOT_SUBMIT = ['add to','adicionar','zu deinem','hinzufügen','ajouter','añadir','your post','seu post','tu publicación','report','denunciar','signaler','melden','share','compartil','teilen','foto','photo','vídeo','video','emoji','gif','sticker','boost','turbinar','impulsionar','promote','promover','promocionar','promouvoir','anuncie','advertise','bewerben'];
|
|
672
|
+
const PERSONAL = ["what's on your mind","no que você está pensando","qué estás pensando","à quoi pensez-vous","woran denkst du","a cosa stai pensando"];
|
|
673
|
+
const COMMENT = ['comente como','comment as','yorum yap','commenter en tant','kommentieren als','comenta como','escreva um comentário','write a comment'];
|
|
674
|
+
// Create-post trigger phrases (the div[role=button] you click to OPEN the composer dialog).
|
|
675
|
+
const TRIGGER = ['escreva algo','write something','no que você está pensando',"what's on your mind",'bir şeyler yaz','exprime-toi','écrivez quelque chose','schreib etwas','escribe algo','crie um post','crie uma publicação','criar publicação','comece uma publicação','publique algo'];
|
|
676
|
+
const PENDING = ['pending approval','will be reviewed','aguardando aprovação','será revisado','en attente d','zur überprüfung','pendiente de aprobación','in attesa di approvazione'];
|
|
677
|
+
const FAILED = ['something went wrong','algo deu errado','tente novamente','try again','unable to post','não foi possível publicar','une erreur s','etwas ist sch'];
|
|
678
|
+
const CREATE_HEADING = ['create post','criar publicação','criar post','créer une publication','beitrag erstellen','crear publicación','crea post'];
|
|
679
|
+
const lc = s => (s || '').toLowerCase();
|
|
680
|
+
// Visible = has real box AND not hidden. Guards against FB's permanent 0x0 hidden dialogs
|
|
681
|
+
// (e.g. the always-present Notifications [role="dialog"]) faking modal.open.
|
|
682
|
+
const visEl = el => { if (!el) return false; const r = el.getBoundingClientRect(); const cs = getComputedStyle(el); return r.width > 8 && r.height > 8 && cs.visibility !== 'hidden' && cs.display !== 'none' && el.getAttribute('aria-hidden') !== 'true'; };
|
|
683
|
+
const vis = el => { const r = el.getBoundingClientRect(); return r.width > 100 && r.height > 20 && getComputedStyle(el).visibility !== 'hidden' && getComputedStyle(el).display !== 'none'; };
|
|
684
|
+
|
|
685
|
+
// Pick the POSTING dialog: visible only, prefer one that contains a composer or whose
|
|
686
|
+
// heading is a create-post heading. Never select hidden/notifications/menu dialogs.
|
|
687
|
+
const allDialogs = [...document.querySelectorAll('[role="dialog"], [aria-modal="true"]')].filter(visEl);
|
|
688
|
+
const scoreDialog = d => {
|
|
689
|
+
const heading = lc(d.querySelector('h2, [role="heading"]')?.innerText || '');
|
|
690
|
+
const hasComposer = !!d.querySelector('[role="textbox"], [contenteditable="true"]');
|
|
691
|
+
const isCreate = CREATE_HEADING.some(h => heading.includes(h));
|
|
692
|
+
return (hasComposer ? 2 : 0) + (isCreate ? 1 : 0);
|
|
693
|
+
};
|
|
694
|
+
const dialog = allDialogs.slice().sort((a, b) => scoreDialog(b) - scoreDialog(a)).find(d => scoreDialog(d) > 0) || null;
|
|
695
|
+
const modal = {
|
|
696
|
+
open: !!dialog,
|
|
697
|
+
title: dialog ? lc(dialog.querySelector('h2, [role="heading"]')?.innerText || '').slice(0, 80) : null,
|
|
698
|
+
closeSelector: dialog ? '[role="dialog"] [aria-label="Close"], [role="dialog"] [aria-label="Fechar"], [role="dialog"] [aria-label="Fermer"], [role="dialog"] [aria-label="Cerrar"], [role="dialog"] [aria-label="Schließen"]' : null,
|
|
699
|
+
containsComposer: dialog ? !!dialog.querySelector('[role="textbox"], [contenteditable="true"]') : false
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const boxes = [...document.querySelectorAll('[role="textbox"], div[contenteditable="true"]')];
|
|
703
|
+
let composers = boxes.map((el, index) => {
|
|
704
|
+
const r = el.getBoundingClientRect();
|
|
705
|
+
const inDialog = !!el.closest('[role="dialog"], [aria-modal="true"]');
|
|
706
|
+
const label = el.getAttribute('aria-label') || '';
|
|
707
|
+
const placeholder = el.getAttribute('placeholder') || el.getAttribute('data-placeholder') || '';
|
|
708
|
+
const text = (el.innerText || '').slice(0, 120);
|
|
709
|
+
const ctx = lc(label + ' ' + placeholder + ' ' + text);
|
|
710
|
+
const isSearch = ctx.includes('search') || ctx.includes('pesquis') || ctx.includes('buscar') || ctx.includes('recherch');
|
|
711
|
+
const isComment = COMMENT.some(p => ctx.includes(p));
|
|
712
|
+
return {
|
|
713
|
+
index, scope: inDialog ? 'dialog' : 'inline', inDialog,
|
|
714
|
+
visible: vis(el),
|
|
715
|
+
isSearch, isComment,
|
|
716
|
+
rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
|
|
717
|
+
text, ariaLabel: label, placeholder,
|
|
718
|
+
isPersonalProfile: PERSONAL.some(p => ctx.includes(p)),
|
|
719
|
+
// A comment box is NOT a post composer; never type a post into it.
|
|
720
|
+
isPostComposer: !isSearch && !isComment && !PERSONAL.some(p => ctx.includes(p)),
|
|
721
|
+
suggestedSelector: inDialog ? '[role="dialog"] [role="textbox"]' : '[role="main"] [role="textbox"]'
|
|
722
|
+
};
|
|
723
|
+
}).filter(c => !c.isSearch);
|
|
724
|
+
|
|
725
|
+
// Create-post TRIGGER (the button to OPEN the composer). Reported so a loop on a bare
|
|
726
|
+
// feed knows what to click first — the probe can only see textboxes that already exist.
|
|
727
|
+
const triggerEl = [...document.querySelectorAll('[role="main"] [role="button"], [role="main"] div[role="button"]')]
|
|
728
|
+
.find(b => { const t = lc(b.innerText); return TRIGGER.some(p => t.includes(p)) && vis(b) && !b.closest('[role="article"]'); });
|
|
729
|
+
const trigger = triggerEl ? {
|
|
730
|
+
found: true,
|
|
731
|
+
text: (triggerEl.innerText || '').trim().slice(0, 60),
|
|
732
|
+
suggestedSelector: '[role="main"] div[role="button"]:has-text("' + (TRIGGER.find(p => lc(triggerEl.innerText).includes(p)) || '') + '")'
|
|
733
|
+
} : { found: false, text: null, suggestedSelector: null };
|
|
734
|
+
|
|
735
|
+
// Submit button scoped to the chosen VISIBLE posting dialog (fall back to main feed composer).
|
|
736
|
+
// Detection honors this script's design intent (role+aria+geometry over translated strings):
|
|
737
|
+
// a locale/approval-tolerant label match first, then a conservative structural footer-button
|
|
738
|
+
// fallback so an untranslated label can never strand the agent with submitButton:null again.
|
|
739
|
+
const root = dialog || document.querySelector('[role="main"]') || document.body;
|
|
740
|
+
const labelOf = b => (b.innerText || b.getAttribute('aria-label') || '').trim();
|
|
741
|
+
const enabledOf = b => b.getAttribute('aria-disabled') !== 'true' && !b.disabled;
|
|
742
|
+
const wc = s => s ? s.trim().split(/\\s+/).length : 0;
|
|
743
|
+
const isSubmitLabel = raw => {
|
|
744
|
+
const t = lc(raw).trim();
|
|
745
|
+
if (!t || wc(t) > 5) return false; // submit labels are short
|
|
746
|
+
if (NOT_SUBMIT.some(n => t.includes(n))) return false;
|
|
747
|
+
return PUB.some(p => t === p || t.startsWith(p + ' ') || t.endsWith(' ' + p));
|
|
748
|
+
};
|
|
749
|
+
const btns = [...root.querySelectorAll('[role="button"], button')].filter(vis);
|
|
750
|
+
let submitEl = btns.find(b => isSubmitLabel(labelOf(b)));
|
|
751
|
+
let matchType = submitEl ? 'label' : null;
|
|
752
|
+
// Structural fallback: only inside a real posting dialog (never the bare feed). FB's Post
|
|
753
|
+
// button is a near-full-width control in the dialog footer; pick the last such candidate
|
|
754
|
+
// that is not the Close button or a toolbar item. Flagged heuristic so callers can verify.
|
|
755
|
+
if (!submitEl && dialog) {
|
|
756
|
+
const dr = dialog.getBoundingClientRect();
|
|
757
|
+
const cands = btns.filter(b => {
|
|
758
|
+
const r = b.getBoundingClientRect();
|
|
759
|
+
const t = lc(labelOf(b));
|
|
760
|
+
return labelOf(b) && wc(t) <= 5 && !NOT_SUBMIT.some(n => t.includes(n))
|
|
761
|
+
&& !/close|fechar|fermer|cerrar|schließen|kapat|chiudi|sluiten/.test(t)
|
|
762
|
+
&& r.width >= dr.width * 0.5 && r.top >= dr.top + dr.height * 0.45;
|
|
763
|
+
});
|
|
764
|
+
if (cands.length) { submitEl = cands[cands.length - 1]; matchType = 'heuristic'; }
|
|
765
|
+
}
|
|
766
|
+
const submitButton = submitEl ? {
|
|
767
|
+
label: labelOf(submitEl),
|
|
768
|
+
scope: dialog ? 'dialog' : 'inline',
|
|
769
|
+
matchType,
|
|
770
|
+
selector: (dialog ? '[role="dialog"] ' : '[role="main"] ') + '[role="button"]:has-text("' + labelOf(submitEl) + '")',
|
|
771
|
+
enabled: enabledOf(submitEl)
|
|
772
|
+
} : null;
|
|
773
|
+
|
|
774
|
+
const bodyText = lc(document.body.innerText).slice(0, 6000);
|
|
775
|
+
let state = 'unknown', evidence = '';
|
|
776
|
+
const pendingHit = PENDING.find(p => bodyText.includes(p));
|
|
777
|
+
const failedHit = FAILED.find(p => bodyText.includes(p));
|
|
778
|
+
if (pendingHit) { state = 'pending-approval'; evidence = pendingHit; }
|
|
779
|
+
else if (failedHit) { state = 'posting-failed'; evidence = failedHit; }
|
|
780
|
+
else if (modal.open && modal.containsComposer) { state = 'composer-open'; evidence = modal.title || 'dialog open'; }
|
|
781
|
+
// No posting dialog, no composer focused, but a create-post trigger exists => idle feed,
|
|
782
|
+
// safe to treat a subsequent successful submit (dialog gone, no error) as visible-post.
|
|
783
|
+
else if (!modal.open && !composers.some(c => c.isPostComposer && c.visible)) {
|
|
784
|
+
state = trigger.found ? 'visible-post' : 'unknown';
|
|
785
|
+
evidence = trigger.found ? 'no composer open; create-post trigger present' : '';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// recommendedAction: a deterministic next step so a one-shot / resumed iteration knows what
|
|
789
|
+
// to do without re-deriving it from prose (this drives the exec-loop RESUME CHECK preamble).
|
|
790
|
+
const postBox = composers.find(c => c.isPostComposer && c.visible);
|
|
791
|
+
let recommendedAction = 'unknown';
|
|
792
|
+
if (state === 'pending-approval' || state === 'visible-post') recommendedAction = 'done';
|
|
793
|
+
else if (state === 'posting-failed') recommendedAction = 'retry';
|
|
794
|
+
else if (submitButton && submitButton.enabled && postBox && postBox.text.trim()) recommendedAction = 'submit';
|
|
795
|
+
else if (postBox && !postBox.text.trim()) recommendedAction = 'type';
|
|
796
|
+
else if (modal.open && submitButton && submitButton.enabled) recommendedAction = 'submit';
|
|
797
|
+
else if (!modal.open && trigger.found) recommendedAction = 'open-composer';
|
|
798
|
+
|
|
799
|
+
// Money-safety: detect Boost/Promote controls so the agent NEVER mistakes one for the
|
|
800
|
+
// Post button. FB renders "Turbinar publicação" / "Boost post" beside Page posts (PT-BR
|
|
801
|
+
// on this account) and a "Boost when published" toggle in the composer. SynaBun posts
|
|
802
|
+
// organically only — surface these as a risk; they must never be clicked.
|
|
803
|
+
const BOOST = ['boost','turbinar','impulsionar','promote','promover','promocionar','anuncie','advertise'];
|
|
804
|
+
const boostEls = [...document.querySelectorAll('[role="button"], button, a[role="link"], a')]
|
|
805
|
+
.filter(b => { const t = lc(b.innerText || b.getAttribute('aria-label') || ''); return t && BOOST.some(p => t.includes(p)) && visEl(b); });
|
|
806
|
+
const boostToggle = [...document.querySelectorAll('[role="switch"], [role="checkbox"], input[type="checkbox"]')]
|
|
807
|
+
.find(t => { const lab = lc((t.getAttribute('aria-label') || '') + ' ' + (t.closest('label')?.innerText || '') + ' ' + (t.parentElement?.innerText || '')); return BOOST.some(p => lab.includes(p)); });
|
|
808
|
+
const boostRisk = {
|
|
809
|
+
present: boostEls.length > 0 || !!boostToggle,
|
|
810
|
+
controls: boostEls.slice(0, 4).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().slice(0, 40)),
|
|
811
|
+
boostWhenPublishedToggle: boostToggle ? { on: boostToggle.getAttribute('aria-checked') === 'true' || boostToggle.checked === true } : null,
|
|
812
|
+
warning: (boostEls.length > 0 || !!boostToggle) ? 'PAID Boost/Promote control present — NEVER click it; submit ONLY via submitButton.selector. If a "Boost when published" toggle is ON, turn it OFF first. SynaBun posts organically only.' : null
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
return { modal, composers, trigger, submitButton, submission: { state, evidence }, recommendedAction, boostRisk };
|
|
816
|
+
})()
|
|
817
|
+
`.trim();
|
|
818
|
+
export const browserFbComposerStateSchema = {
|
|
819
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
820
|
+
tabId: tabIdField,
|
|
821
|
+
};
|
|
822
|
+
export const browserFbComposerStateDescription = 'Inspect the Facebook posting surface in ONE call (read-only). Returns: modal {open,title,closeSelector,containsComposer} ' +
|
|
823
|
+
'(modal.open is true ONLY for a visible posting dialog — FBs permanent hidden 0x0 Notifications dialog is ignored); ' +
|
|
824
|
+
'trigger {found,text,suggestedSelector} — the Create-post button to CLICK first when no composer dialog is open yet (on a bare feed the probe can only see boxes that already exist, so use this to OPEN the composer; after opening, browser_type with NO selector into the auto-focused editor — selector-targeting the dialog editor is flaky); ' +
|
|
825
|
+
'composers[] (search boxes filtered out) each with scope inline|dialog, inDialog, visible, rect, text, ariaLabel, placeholder, ' +
|
|
826
|
+
'isPersonalProfile, isComment, isPostComposer (a comment box is flagged isComment and is NEVER a post composer — do not type a post into it), suggestedSelector; ' +
|
|
827
|
+
'submitButton {label,scope,matchType,selector,enabled} scoped to the active visible dialog (matchType "label" = matched a publish/approval verb incl. German Senden/Veröffentlichen/Zur Genehmigung senden; "heuristic" = inferred as the dialog footer primary button when the label was untranslated, so verify via submission.state after clicking); ' +
|
|
828
|
+
'submission {state: visible-post|pending-approval|posting-failed|composer-open|unknown, evidence}; and ' +
|
|
829
|
+
'recommendedAction {submit|type|open-composer|done|retry|unknown} — the deterministic next step (submit = composer open with text AND an enabled submit button; use it to FINISH a half-typed post before starting anything new). ' +
|
|
830
|
+
'Locale-tolerant (PT/EN/TR/FR/ES/DE incl. approval-flow groups). Flow: probe; if recommendedAction is "submit", click submitButton.selector and confirm FIRST; else if no composer dialog, click trigger.suggestedSelector; re-probe; type the full post into the auto-focused isPostComposer box with browser_type mode:"insert" (blank lines between paragraphs, link alone on the final line — insertText keeps the paragraphs and the isolated URL without firing Enter, so the typeahead cannot hijack it; do NOT use mode:"paragraphs" on the modal, its Escape closes the dialog); wait for submitButton.enabled; submit via submitButton.selector; confirm via submission.state. ' +
|
|
831
|
+
'Modal-aware: the group Create-post dialog is a valid surface; reject only personal-profile/share dialogs. ' +
|
|
832
|
+
'Also returns boostRisk {present, controls[], boostWhenPublishedToggle, warning}: if present, a PAID Boost/Promote/Turbinar control is on the page — NEVER click it and never submit via it (SynaBun posts organically only); submit ONLY via submitButton.selector and turn any "Boost when published" toggle OFF first. ' +
|
|
833
|
+
'When recommendedAction or submission.state is "unknown", the submit match is "heuristic", or an open composer has no submitButton, an additive `judgment` {advisory:true, state, confidence, suggestedControl? {purpose, label, selector, enabled, confidence}, note} may appear: a Jev reading built from booleans, counts and the dialog button labels only (never the post text), with Boost/Promote controls removed before it is asked. It never changes recommendedAction, submitButton or boostRisk; treat suggestedControl as a hint to verify with a fresh probe, not as a selector to click blindly.';
|
|
834
|
+
export async function handleBrowserFbComposerState(args) {
|
|
835
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
836
|
+
if ('error' in resolved)
|
|
837
|
+
return text(resolved.error);
|
|
838
|
+
const result = await ni.evaluate(resolved.sessionId, FB_COMPOSER_STATE_SCRIPT, resolved.tabId);
|
|
839
|
+
if (result.error)
|
|
840
|
+
return text(`Composer state probe failed: ${result.error}`);
|
|
841
|
+
// Only when the probe could not decide (unknown state or action, no submit button, or a heuristic match).
|
|
842
|
+
// Jev sees an allow-list pick of the probe and short button names — never the draft, the trigger text or a title.
|
|
843
|
+
const judgment = await assist.judgeComposer('facebook', result.result, resolved.sessionId, resolved.tabId).catch(() => null);
|
|
844
|
+
return text(JSON.stringify(judgment && result.result && typeof result.result === 'object' ? { ...result.result, judgment } : result.result));
|
|
845
|
+
}
|
|
846
|
+
// ── browser_extract_fb_groups ──
|
|
847
|
+
// Scrape the JOINED-groups list from facebook.com/groups/joins/ so a posting/seeding loop
|
|
848
|
+
// gets a deterministic, region-sortable index in one call instead of eyeballing the rail.
|
|
849
|
+
// FB's joins page does NOT expose region, so the handler reconciles each group URL against
|
|
850
|
+
// the curated seed-queue memory (url | lang | currency) and falls back to a name lexicon.
|
|
851
|
+
const FB_GROUPS_EXTRACTOR_SCRIPT = `
|
|
852
|
+
(() => {
|
|
853
|
+
const root = document.querySelector('[role="main"]') || document.body;
|
|
854
|
+
const seen = new Set();
|
|
855
|
+
const out = [];
|
|
856
|
+
// /groups/<slug>/ links — exclude FB's own section routes (feed, joins, discover, ...).
|
|
857
|
+
const SKIP = new Set(['feed','joins','discover','create','category','search','your_groups','requests','notifications']);
|
|
858
|
+
const norm = h => { try { const u = new URL(h, location.origin); return (u.origin + u.pathname.replace(/\\/+$/, '') + '/'); } catch { return null; } };
|
|
859
|
+
for (const a of root.querySelectorAll('a[href*="/groups/"]')) {
|
|
860
|
+
const m = (a.getAttribute('href') || '').match(/\\/groups\\/([^/?#]+)\\/?/);
|
|
861
|
+
if (!m) continue;
|
|
862
|
+
const slug = m[1];
|
|
863
|
+
if (SKIP.has(slug)) continue;
|
|
864
|
+
const url = norm(a.href);
|
|
865
|
+
if (!url || seen.has(url)) continue;
|
|
866
|
+
// Name: prefer aria-label, then a strong/heading child, then link text (first line only).
|
|
867
|
+
let name = (a.getAttribute('aria-label') || '').trim();
|
|
868
|
+
if (!name) name = (a.querySelector('strong, span[dir="auto"], h2, h3')?.innerText || '').trim();
|
|
869
|
+
if (!name) name = (a.innerText || '').trim().split('\\n')[0];
|
|
870
|
+
if (!name) continue;
|
|
871
|
+
// Subtitle (last-active / privacy / member count) from the surrounding card.
|
|
872
|
+
const card = a.closest('[role="listitem"], li, div[class]') || a.parentElement;
|
|
873
|
+
const sub = card ? (card.innerText || '').replace(name, '').replace(/\\s+/g, ' ').trim().slice(0, 120) : '';
|
|
874
|
+
seen.add(url);
|
|
875
|
+
out.push({ name: name.slice(0, 120), url, subtitle: sub });
|
|
876
|
+
}
|
|
877
|
+
return out;
|
|
878
|
+
})()
|
|
879
|
+
`.trim();
|
|
880
|
+
// Region maps (REGION_BY_CURRENCY / REGION_BY_LANG / NAME_REGION), inferRegionFromName,
|
|
881
|
+
// normUrl, and parseSeedQueue now live in ../services/fb-regions.ts — shared with the
|
|
882
|
+
// fb_groups directory tool so the live extractor and the structured store stay in lockstep.
|
|
883
|
+
// Approximate member count from a localized joins-card subtitle ("Public group · 12K members").
|
|
884
|
+
function parseMembers(s) {
|
|
885
|
+
if (!s)
|
|
886
|
+
return null;
|
|
887
|
+
const m = String(s).match(/([\d.,]+)\s*([kKmM])?/);
|
|
888
|
+
if (!m)
|
|
889
|
+
return null;
|
|
890
|
+
const n = parseFloat(m[1].replace(/,/g, ''));
|
|
891
|
+
if (isNaN(n))
|
|
892
|
+
return null;
|
|
893
|
+
const suf = (m[2] || '').toLowerCase();
|
|
894
|
+
return Math.round(suf === 'k' ? n * 1e3 : suf === 'm' ? n * 1e6 : n);
|
|
895
|
+
}
|
|
896
|
+
export const browserExtractFbGroupsSchema = {
|
|
897
|
+
...pagedExtractorFields,
|
|
898
|
+
maxChars: z.coerce.number().int().min(2).optional().describe('Cap the complete grouped JSON response, preserving whole groups. Metadata must fit; unmatched duplicates are omitted first.'),
|
|
899
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
900
|
+
tabId: tabIdField,
|
|
901
|
+
region: z.string().optional().describe('Optional case-insensitive filter: only return groups in this region bucket (UK, US, EU, Brazil, LatAm, Turkey, Australia, Canada, Unknown).'),
|
|
902
|
+
seedQueueId: z.string().optional().describe('Memory id of the curated seed-queue (url | lang | currency) used to reconcile region/currency. Default: the built-in seed-queue memory id.'),
|
|
903
|
+
};
|
|
904
|
+
export const browserExtractFbGroupsDescription = 'Extract ALL JOINED Facebook groups from the current page (navigate to facebook.com/groups/joins/?ordering=viewer_added first) as a region-sorted index. ' +
|
|
905
|
+
'Auto-scrolls the full virtualized joins list and dedupes by url in ONE call (defaults: scrolls:10, maxItems:1000 — override for very large/small lists). ' +
|
|
906
|
+
'Each group {name, url, subtitle, member_count, region, lang, currency, source} is reconciled against the curated seed-queue memory first; groups not in it are classified from name+slug ' +
|
|
907
|
+
'(language tokens: pt/tr/de/fr/es/pl/it/nl, and country aliases) into region + posting-currency, source:"name-heuristic"; the still-ambiguous ones land in region "Unknown" and under "unmatched". Spanish groups split into LatAm (USD) vs Spain (EUR). ' +
|
|
908
|
+
'Returns { counts, byRegion: { UK:[...], US:[...], EU:[...], Brazil:[...], LatAm:[...], ... }, unmatched:[...] }. Pass region to filter to one bucket. ' +
|
|
909
|
+
'Feed the whole JSON to fb_groups import {fromExtract} to populate/refresh the structured directory.';
|
|
910
|
+
export async function handleBrowserExtractFbGroups(args) {
|
|
911
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
912
|
+
if ('error' in resolved)
|
|
913
|
+
return text(resolved.error);
|
|
914
|
+
// Full-scroll the virtualized joins list, accumulating + deduping by url. Defaults harvest the
|
|
915
|
+
// whole list in one call; the caller can override scrolls/maxItems.
|
|
916
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, FB_GROUPS_EXTRACTOR_SCRIPT, {
|
|
917
|
+
scrolls: args.scrolls ?? 10,
|
|
918
|
+
minItems: args.minItems,
|
|
919
|
+
maxItems: args.maxItems ?? 1000,
|
|
920
|
+
timeoutMs: args.timeoutMs,
|
|
921
|
+
dedupeKeys: ['url'],
|
|
922
|
+
scrollIfEmpty: true,
|
|
923
|
+
});
|
|
924
|
+
if ('error' in res)
|
|
925
|
+
return text(`Extract failed: ${res.error}`);
|
|
926
|
+
const groups = res.items;
|
|
927
|
+
if (groups.length === 0) {
|
|
928
|
+
return emptyExtraction(resolved, 'browser_extract_fb_groups', 'the Facebook joined-groups list', 'No joined groups found. Make sure you navigated to facebook.com/groups/joins/?ordering=viewer_added and the list rendered — browser_scroll down then retry.');
|
|
929
|
+
}
|
|
930
|
+
// Reconcile with the curated seed-queue memory (url | lang | currency).
|
|
931
|
+
const seedId = args.seedQueueId || 'dc301b83-4859-4a55-9713-03a5c0a12197';
|
|
932
|
+
let seedMap = new Map();
|
|
933
|
+
try {
|
|
934
|
+
const mem = await getMemory(seedId);
|
|
935
|
+
if (mem?.payload?.content)
|
|
936
|
+
seedMap = parseSeedQueue(mem.payload.content);
|
|
937
|
+
}
|
|
938
|
+
catch { /* seed-queue optional — fall back to name heuristics */ }
|
|
939
|
+
const tagged = groups.map(g => {
|
|
940
|
+
const name = cleanName(g.name) || g.name;
|
|
941
|
+
const member_count = parseMembers(g.subtitle);
|
|
942
|
+
const seed = seedMap.get(g.url);
|
|
943
|
+
if (seed) {
|
|
944
|
+
// Curated seed currency/lang is authoritative; regionForSeed now resolves USD+es -> LatAm.
|
|
945
|
+
return { ...g, name, member_count, region: regionForSeed(seed.lang, seed.currency), lang: seed.lang || null, currency: seed.currency || null, source: 'seed-queue' };
|
|
946
|
+
}
|
|
947
|
+
const r = resolveClassification({ name, url: g.url });
|
|
948
|
+
return { ...g, name, member_count, region: r.region, lang: r.lang, currency: r.currency, source: 'name-heuristic' };
|
|
949
|
+
});
|
|
950
|
+
const filter = (args.region || '').trim().toLowerCase();
|
|
951
|
+
const visible = filter ? tagged.filter(g => g.region.toLowerCase() === filter) : tagged;
|
|
952
|
+
const sorted = visible.slice().sort((a, b) => a.region.localeCompare(b.region) || a.name.localeCompare(b.name));
|
|
953
|
+
const unmatched = tagged.filter(g => g.source === 'name-heuristic').map(g => ({ name: g.name, url: g.url, region: g.region }));
|
|
954
|
+
const project = (item) => args.fields
|
|
955
|
+
? Object.fromEntries(args.fields.filter(key => Object.hasOwn(item, key)).map(key => [key, item[key]])) : item;
|
|
956
|
+
const payloadFor = (count, includeUnmatched, capped) => {
|
|
957
|
+
const byRegion = {};
|
|
958
|
+
for (const group of sorted.slice(0, count))
|
|
959
|
+
(byRegion[group.region] ||= []).push(project(group));
|
|
960
|
+
return {
|
|
961
|
+
counts: {
|
|
962
|
+
total: tagged.length,
|
|
963
|
+
shown: count,
|
|
964
|
+
inSeedQueue: tagged.filter(g => g.source === 'seed-queue').length,
|
|
965
|
+
unmatched: unmatched.length,
|
|
966
|
+
byRegion: Object.fromEntries(Object.entries(byRegion).map(([r, gs]) => [r, gs.length])),
|
|
967
|
+
},
|
|
968
|
+
byRegion,
|
|
969
|
+
unmatched: includeUnmatched ? unmatched.map(project) : [],
|
|
970
|
+
extraction: { raw: res.raw, scrollsUsed: res.scrollsUsed, partial: !!res.partial, truncated: res.truncated || capped, stopReason: capped ? 'max_chars' : res.stopReason, ...(res.failureReason ? { failureReason: res.failureReason } : {}) },
|
|
971
|
+
};
|
|
972
|
+
};
|
|
973
|
+
let body = JSON.stringify(payloadFor(sorted.length, true, false));
|
|
974
|
+
if (args.maxChars !== undefined && body.length > args.maxChars) {
|
|
975
|
+
// Classification and region filtering happen before projection. Keep counts
|
|
976
|
+
// truthful while removing duplicate unmatched entries before actual groups.
|
|
977
|
+
body = JSON.stringify(payloadFor(sorted.length, false, true));
|
|
978
|
+
if (body.length > args.maxChars) {
|
|
979
|
+
let low = 0;
|
|
980
|
+
let high = sorted.length;
|
|
981
|
+
while (low < high) {
|
|
982
|
+
const mid = Math.ceil((low + high) / 2);
|
|
983
|
+
if (JSON.stringify(payloadFor(mid, false, true)).length <= args.maxChars)
|
|
984
|
+
low = mid;
|
|
985
|
+
else
|
|
986
|
+
high = mid - 1;
|
|
987
|
+
}
|
|
988
|
+
body = JSON.stringify(payloadFor(low, false, true));
|
|
989
|
+
if (body.length > args.maxChars)
|
|
990
|
+
return text('Extract failed: maxChars is too small for grouped response metadata. Increase maxChars.');
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return text(body);
|
|
994
|
+
}
|
|
995
|
+
// ── browser_extract_tiktok_videos ──
|
|
996
|
+
const TIKTOK_FEED_EXTRACTOR_SCRIPT = `
|
|
997
|
+
(() => {
|
|
998
|
+
// Build a map of handle -> video metadata from global state (gives videoUrl, caption, music)
|
|
999
|
+
const globalItems = window['__$UNIVERSAL_DATA$__']?.['__DEFAULT_SCOPE__']?.['webapp.updated-items'] || {};
|
|
1000
|
+
const stateMap = {};
|
|
1001
|
+
for (const key of Object.keys(globalItems)) {
|
|
1002
|
+
const item = globalItems[key];
|
|
1003
|
+
const h = item?.author?.uniqueId;
|
|
1004
|
+
if (h) stateMap[h] = { videoUrl: 'https://www.tiktok.com/@' + h + '/video/' + item.id, caption: item.desc || null, music: item.music?.title || null };
|
|
1005
|
+
}
|
|
1006
|
+
// Merge DOM counts (live) with state metadata
|
|
1007
|
+
return Array.from(document.querySelectorAll('article')).filter(el =>
|
|
1008
|
+
el.querySelector('[data-e2e="like-count"]')
|
|
1009
|
+
).map(el => {
|
|
1010
|
+
const handle = el.querySelector('a[href^="/@"]')?.getAttribute('href')?.replace('/@','')?.split('?')[0] || null;
|
|
1011
|
+
const state = handle ? (stateMap[handle] || {}) : {};
|
|
1012
|
+
const likes = el.querySelector('[data-e2e="like-count"]')?.innerText?.trim() || null;
|
|
1013
|
+
const comments = el.querySelector('[data-e2e="comment-count"]')?.innerText?.trim() || null;
|
|
1014
|
+
const saves = el.querySelector('[data-e2e="undefined-count"]')?.innerText?.trim() || null;
|
|
1015
|
+
const shares = el.querySelector('[data-e2e="share-count"]')?.innerText?.trim() || null;
|
|
1016
|
+
const caption = state.caption || el.querySelector('[data-e2e="video-desc"]')?.innerText?.trim() || null;
|
|
1017
|
+
return { handle, videoUrl: state.videoUrl || null, caption, likes, comments, saves, shares, music: state.music || null };
|
|
1018
|
+
}).filter(v => v.handle);
|
|
1019
|
+
})()
|
|
1020
|
+
`.trim();
|
|
1021
|
+
export const browserExtractTiktokVideosSchema = {
|
|
1022
|
+
...pagedExtractorFields,
|
|
1023
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1024
|
+
tabId: tabIdField,
|
|
1025
|
+
};
|
|
1026
|
+
export const browserExtractTiktokVideosDescription = 'Extract visible TikTok videos from the For You or Following feed as structured JSON ' +
|
|
1027
|
+
'(handle, videoUrl, caption, likes, comments, saves, shares, music). ' +
|
|
1028
|
+
'Navigate to tiktok.com/ or tiktok.com/following first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. ' +
|
|
1029
|
+
'Much faster than browser_snapshot for data harvesting from TikTok feeds.';
|
|
1030
|
+
export async function handleBrowserExtractTiktokVideos(args) {
|
|
1031
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1032
|
+
if ('error' in resolved)
|
|
1033
|
+
return text(resolved.error);
|
|
1034
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, TIKTOK_FEED_EXTRACTOR_SCRIPT, {
|
|
1035
|
+
...args, dedupeKeys: ['videoUrl', 'handle'], scrollIfEmpty: true,
|
|
1036
|
+
});
|
|
1037
|
+
if ('error' in res)
|
|
1038
|
+
return text(`Extract failed: ${res.error}`);
|
|
1039
|
+
if (res.items.length === 0) {
|
|
1040
|
+
return emptyExtraction(resolved, 'browser_extract_tiktok_videos', 'the TikTok For You or Following feed', 'No TikTok videos found. Make sure you are on tiktok.com/ or tiktok.com/following, then retry with scrolls:2.');
|
|
1041
|
+
}
|
|
1042
|
+
return text(formatExtraction('video', res));
|
|
1043
|
+
}
|
|
1044
|
+
// ── browser_extract_tiktok_search ──
|
|
1045
|
+
const TIKTOK_SEARCH_EXTRACTOR_SCRIPT = `
|
|
1046
|
+
Array.from(document.querySelectorAll('[data-e2e="search_top-item"]')).map(el => {
|
|
1047
|
+
const videoLink = el.querySelector('a[href*="/video/"]');
|
|
1048
|
+
const userLink = el.querySelector('[data-e2e="search-card-user-link"]');
|
|
1049
|
+
const caption = el.querySelector('[data-e2e="search-card-video-caption"]');
|
|
1050
|
+
const uniqueId = el.querySelector('[data-e2e="search-card-user-unique-id"]');
|
|
1051
|
+
const views = el.querySelector('[data-e2e="video-views"]');
|
|
1052
|
+
return {
|
|
1053
|
+
videoUrl: videoLink ? videoLink.href : null,
|
|
1054
|
+
handle: uniqueId ? uniqueId.innerText.trim() : null,
|
|
1055
|
+
profileUrl: userLink ? userLink.href : null,
|
|
1056
|
+
caption: caption ? caption.innerText.trim() : null,
|
|
1057
|
+
views: views ? views.innerText.trim() : null,
|
|
1058
|
+
};
|
|
1059
|
+
}).filter(v => v.videoUrl)
|
|
1060
|
+
`.trim();
|
|
1061
|
+
export const browserExtractTiktokSearchSchema = {
|
|
1062
|
+
...pagedExtractorFields,
|
|
1063
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1064
|
+
tabId: tabIdField,
|
|
1065
|
+
};
|
|
1066
|
+
export const browserExtractTiktokSearchDescription = 'Extract visible TikTok search result videos as structured JSON ' +
|
|
1067
|
+
'(videoUrl, handle, profileUrl, caption, views). ' +
|
|
1068
|
+
'Navigate to tiktok.com/search?q=<query> first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. ' +
|
|
1069
|
+
'Much faster than browser_snapshot for harvesting TikTok search results.';
|
|
1070
|
+
export async function handleBrowserExtractTiktokSearch(args) {
|
|
1071
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1072
|
+
if ('error' in resolved)
|
|
1073
|
+
return text(resolved.error);
|
|
1074
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, TIKTOK_SEARCH_EXTRACTOR_SCRIPT, {
|
|
1075
|
+
...args, dedupeKeys: ['videoUrl'], scrollIfEmpty: true,
|
|
1076
|
+
});
|
|
1077
|
+
if ('error' in res)
|
|
1078
|
+
return text(`Extract failed: ${res.error}`);
|
|
1079
|
+
if (res.items.length === 0) {
|
|
1080
|
+
return emptyExtraction(resolved, 'browser_extract_tiktok_search', 'TikTok search results', 'No search results found. Make sure you are on tiktok.com/search?q=... then retry.');
|
|
1081
|
+
}
|
|
1082
|
+
return text(formatExtraction('result', res));
|
|
1083
|
+
}
|
|
1084
|
+
// ── browser_extract_tiktok_studio ──
|
|
1085
|
+
const TIKTOK_STUDIO_EXTRACTOR_SCRIPT = `
|
|
1086
|
+
Array.from(document.querySelectorAll('[data-tt="components_PostInfoCell_a"]')).map(link => {
|
|
1087
|
+
const row = link.closest('[data-tt="components_RowLayout_FlexRow"]') ||
|
|
1088
|
+
link.closest('[data-tt="components_ItemRow_FlexRow"]') ||
|
|
1089
|
+
link.parentElement?.closest('[class*="FlexRow"]');
|
|
1090
|
+
const dateEl = row?.querySelector('[data-tt="components_PublishStageLabel_TUXText"]');
|
|
1091
|
+
const privacyBtn = row?.querySelector('[data-tt="components_PrivacyCell_TUXButton"]');
|
|
1092
|
+
const statEls = row ? Array.from(row.querySelectorAll('[data-tt="components_ItemRow_TUXText"]')) : [];
|
|
1093
|
+
const stats = statEls.map(el => el.innerText.trim()).filter(Boolean);
|
|
1094
|
+
return {
|
|
1095
|
+
title: link.innerText.trim(),
|
|
1096
|
+
url: link.href,
|
|
1097
|
+
date: dateEl ? dateEl.innerText.trim() : null,
|
|
1098
|
+
privacy: privacyBtn ? privacyBtn.innerText.trim() : null,
|
|
1099
|
+
stats,
|
|
1100
|
+
};
|
|
1101
|
+
}).filter(p => p.title)
|
|
1102
|
+
`.trim();
|
|
1103
|
+
export const browserExtractTiktokStudioSchema = {
|
|
1104
|
+
...pagedExtractorFields,
|
|
1105
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1106
|
+
tabId: tabIdField,
|
|
1107
|
+
};
|
|
1108
|
+
export const browserExtractTiktokStudioDescription = 'Extract visible posts from TikTok Studio content list as structured JSON ' +
|
|
1109
|
+
'(title, url, date, privacy, stats[]). ' +
|
|
1110
|
+
'Navigate to tiktok.com/tiktokstudio/content first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. ' +
|
|
1111
|
+
'Use this to audit, manage, or bulk-read your published TikTok content.';
|
|
1112
|
+
export async function handleBrowserExtractTiktokStudio(args) {
|
|
1113
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1114
|
+
if ('error' in resolved)
|
|
1115
|
+
return text(resolved.error);
|
|
1116
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, TIKTOK_STUDIO_EXTRACTOR_SCRIPT, {
|
|
1117
|
+
...args, dedupeKeys: ['url', 'title'],
|
|
1118
|
+
});
|
|
1119
|
+
if ('error' in res)
|
|
1120
|
+
return text(`Extract failed: ${res.error}`);
|
|
1121
|
+
if (res.items.length === 0) {
|
|
1122
|
+
return emptyExtraction(resolved, 'browser_extract_tiktok_studio', 'the TikTok Studio content list', 'No Studio posts found. Make sure you are on tiktok.com/tiktokstudio/content then retry.');
|
|
1123
|
+
}
|
|
1124
|
+
return text(formatExtraction('post', res));
|
|
1125
|
+
}
|
|
1126
|
+
// ── browser_extract_tiktok_profile ──
|
|
1127
|
+
const TIKTOK_PROFILE_EXTRACTOR_SCRIPT = `
|
|
1128
|
+
(() => {
|
|
1129
|
+
const name = document.querySelector('[data-e2e="user-title"]')?.innerText?.trim() || null;
|
|
1130
|
+
const handle = document.querySelector('[data-e2e="user-subtitle"]')?.innerText?.trim() || null;
|
|
1131
|
+
const bio = document.querySelector('[data-e2e="user-bio"]')?.innerText?.trim() || null;
|
|
1132
|
+
const followers = document.querySelector('[data-e2e="followers-count"]')?.innerText?.trim() || null;
|
|
1133
|
+
const following = document.querySelector('[data-e2e="following-count"]')?.innerText?.trim() || null;
|
|
1134
|
+
const likes = document.querySelector('[data-e2e="likes-count"]')?.innerText?.trim() || null;
|
|
1135
|
+
const videoItems = Array.from(document.querySelectorAll('[data-e2e="user-post-item"]')).map(el => {
|
|
1136
|
+
const link = el.querySelector('a[href*="/video/"]');
|
|
1137
|
+
const views = el.querySelector('[data-e2e="video-views"]');
|
|
1138
|
+
return {
|
|
1139
|
+
videoUrl: link ? link.href : null,
|
|
1140
|
+
views: views ? views.innerText.trim() : null,
|
|
1141
|
+
};
|
|
1142
|
+
}).filter(v => v.videoUrl);
|
|
1143
|
+
return { name, handle, bio, followers, following, likes, videos: videoItems };
|
|
1144
|
+
})()
|
|
1145
|
+
`.trim();
|
|
1146
|
+
export const browserExtractTiktokProfileSchema = {
|
|
1147
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1148
|
+
tabId: tabIdField,
|
|
1149
|
+
};
|
|
1150
|
+
export const browserExtractTiktokProfileDescription = 'Extract profile info and video grid from a TikTok profile page as structured JSON ' +
|
|
1151
|
+
'(name, handle, bio, followers, following, likes, videos[{videoUrl, views}]). ' +
|
|
1152
|
+
'Navigate to tiktok.com/@username first. Scroll down to load more videos in the grid. ' +
|
|
1153
|
+
'Use this to audit a creator profile or collect video URLs for further processing.';
|
|
1154
|
+
export async function handleBrowserExtractTiktokProfile(args) {
|
|
1155
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1156
|
+
if ('error' in resolved)
|
|
1157
|
+
return text(resolved.error);
|
|
1158
|
+
const result = await ni.evaluate(resolved.sessionId, TIKTOK_PROFILE_EXTRACTOR_SCRIPT, resolved.tabId);
|
|
1159
|
+
if (result.error)
|
|
1160
|
+
return text(`Extract failed: ${result.error}`);
|
|
1161
|
+
const profile = result.result;
|
|
1162
|
+
if (!profile || (!profile.name && !profile.handle)) {
|
|
1163
|
+
return emptyExtraction(resolved, 'browser_extract_tiktok_profile', 'a TikTok profile page', 'No profile found. Make sure you are on tiktok.com/@username then retry.');
|
|
1164
|
+
}
|
|
1165
|
+
return text(`Profile:\n\n${JSON.stringify(profile)}`);
|
|
1166
|
+
}
|
|
1167
|
+
// ── browser_extract_wa_chats ──
|
|
1168
|
+
const WA_CHATS_EXTRACTOR_SCRIPT = `
|
|
1169
|
+
Array.from(document.querySelectorAll('[aria-label="Lista de conversas"] [role="row"]')).map(row => {
|
|
1170
|
+
const nameSpan = row.querySelector('span[title][dir="auto"]');
|
|
1171
|
+
const allSpans = Array.from(row.querySelectorAll('span')).filter(s => !s.children.length && s.innerText?.trim());
|
|
1172
|
+
const timeSpan = allSpans.find(s => /^\\d|Ontem|Hoje|Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday/.test(s.innerText.trim()));
|
|
1173
|
+
const msgSpans = Array.from(row.querySelectorAll('span[dir="auto"]')).filter(s => !s.getAttribute('title') && s.innerText?.trim());
|
|
1174
|
+
const unreadEl = row.querySelector('[aria-label*="mensagem não lida"]');
|
|
1175
|
+
const mutedEl = row.querySelector('[aria-label="Conversa silenciada"]');
|
|
1176
|
+
const pinnedEl = row.querySelector('[aria-label="Conversa fixada"]');
|
|
1177
|
+
const lastMsg = msgSpans.map(s => s.innerText.trim()).filter(t => t && t !== nameSpan?.innerText?.trim()).join(' ').substring(0, 80) || null;
|
|
1178
|
+
return {
|
|
1179
|
+
name: nameSpan?.getAttribute('title') || nameSpan?.innerText?.trim() || null,
|
|
1180
|
+
lastMsg,
|
|
1181
|
+
time: timeSpan?.innerText?.trim() || null,
|
|
1182
|
+
unreadCount: unreadEl?.innerText?.trim() || null,
|
|
1183
|
+
muted: !!mutedEl,
|
|
1184
|
+
pinned: !!pinnedEl,
|
|
1185
|
+
};
|
|
1186
|
+
}).filter(c => c.name)
|
|
1187
|
+
`.trim();
|
|
1188
|
+
export const browserExtractWaChatsSchema = {
|
|
1189
|
+
...pagedExtractorFields,
|
|
1190
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1191
|
+
tabId: tabIdField,
|
|
1192
|
+
};
|
|
1193
|
+
export const browserExtractWaChatsDescription = 'Extract visible WhatsApp chats from the sidebar as structured JSON ' +
|
|
1194
|
+
'(name, lastMsg, time, unreadCount, muted, pinned). ' +
|
|
1195
|
+
'Must be on web.whatsapp.com with the chat list visible. Pass scrolls/minItems to auto-scroll the sidebar and dedupe in ONE call. ' +
|
|
1196
|
+
'Use browser_click on span[title="Chat Name"] to open a specific chat.';
|
|
1197
|
+
export async function handleBrowserExtractWaChats(args) {
|
|
1198
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1199
|
+
if ('error' in resolved)
|
|
1200
|
+
return text(resolved.error);
|
|
1201
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, WA_CHATS_EXTRACTOR_SCRIPT, {
|
|
1202
|
+
...args, dedupeKeys: ['name'],
|
|
1203
|
+
scrollTarget: `document.querySelector('#pane-side')`,
|
|
1204
|
+
scrollDistance: 600,
|
|
1205
|
+
});
|
|
1206
|
+
if ('error' in res)
|
|
1207
|
+
return text(`Extract failed: ${res.error}`);
|
|
1208
|
+
if (res.items.length === 0) {
|
|
1209
|
+
return emptyExtraction(resolved, 'browser_extract_wa_chats', 'WhatsApp Web with the chat list visible', 'No chats found. Make sure you are on web.whatsapp.com with the chat list visible.');
|
|
1210
|
+
}
|
|
1211
|
+
return text(formatExtraction('chat', res));
|
|
1212
|
+
}
|
|
1213
|
+
// ── browser_extract_wa_messages ──
|
|
1214
|
+
const WA_MESSAGES_EXTRACTOR_SCRIPT = `
|
|
1215
|
+
Array.from(document.querySelectorAll('div.copyable-text[data-pre-plain-text]')).map(el => {
|
|
1216
|
+
const meta = el.getAttribute('data-pre-plain-text') || '';
|
|
1217
|
+
const timeMatch = meta.match(/\\[([^,]+),\\s*([^\\]]+)\\]/);
|
|
1218
|
+
const senderMatch = meta.match(/\\]\\s*([^:]+):/);
|
|
1219
|
+
const isOut = !!el.closest('.message-out');
|
|
1220
|
+
const dataId = el.closest('[data-id]')?.getAttribute('data-id') || null;
|
|
1221
|
+
const textContent = el.innerText?.trim() || null;
|
|
1222
|
+
return {
|
|
1223
|
+
sender: senderMatch ? senderMatch[1].trim() : (isOut ? 'Me' : null),
|
|
1224
|
+
time: timeMatch ? timeMatch[1].trim() : null,
|
|
1225
|
+
date: timeMatch ? timeMatch[2].trim() : null,
|
|
1226
|
+
direction: isOut ? 'out' : 'in',
|
|
1227
|
+
text: textContent,
|
|
1228
|
+
dataId,
|
|
1229
|
+
};
|
|
1230
|
+
}).filter(m => m.text)
|
|
1231
|
+
`.trim();
|
|
1232
|
+
export const browserExtractWaMessagesSchema = {
|
|
1233
|
+
...pagedExtractorFields,
|
|
1234
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1235
|
+
tabId: tabIdField,
|
|
1236
|
+
};
|
|
1237
|
+
export const browserExtractWaMessagesDescription = 'Extract visible messages from the open WhatsApp chat as structured JSON ' +
|
|
1238
|
+
'(sender, time, date, direction, text, dataId). ' +
|
|
1239
|
+
'Open a chat first by clicking span[title="Chat Name"]. Pass scrolls/minItems to auto-scroll UP through history and dedupe in ONE call. ' +
|
|
1240
|
+
'direction is "in" for received and "out" for sent messages.';
|
|
1241
|
+
export async function handleBrowserExtractWaMessages(args) {
|
|
1242
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1243
|
+
if ('error' in resolved)
|
|
1244
|
+
return text(resolved.error);
|
|
1245
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, WA_MESSAGES_EXTRACTOR_SCRIPT, {
|
|
1246
|
+
...args, dedupeKeys: ['dataId'],
|
|
1247
|
+
scrollTarget: SCROLLABLE_ANCESTOR('div.copyable-text[data-pre-plain-text]'),
|
|
1248
|
+
scrollDirection: -1, // history loads upward
|
|
1249
|
+
scrollDistance: 800,
|
|
1250
|
+
});
|
|
1251
|
+
if ('error' in res)
|
|
1252
|
+
return text(`Extract failed: ${res.error}`);
|
|
1253
|
+
if (res.items.length === 0) {
|
|
1254
|
+
return emptyExtraction(resolved, 'browser_extract_wa_messages', 'an open WhatsApp Web chat', 'No messages found. Open a chat first, then retry.');
|
|
1255
|
+
}
|
|
1256
|
+
return text(formatExtraction('message', res));
|
|
1257
|
+
}
|
|
1258
|
+
// ── browser_extract_ig_feed ──
|
|
1259
|
+
const IG_FEED_EXTRACTOR_SCRIPT = `
|
|
1260
|
+
Array.from(document.querySelectorAll('article')).map(el => {
|
|
1261
|
+
const userLink = Array.from(el.querySelectorAll('a[href^="/"]')).find(a => a.getAttribute('href')?.match(/^\\/[^/]+\\/$/) && !a.getAttribute('href').includes('/p/') && !a.getAttribute('href').includes('/reel/') && !a.getAttribute('href').includes('/explore/'));
|
|
1262
|
+
const username = userLink?.textContent?.trim() || null;
|
|
1263
|
+
const profileUrl = userLink ? 'https://www.instagram.com' + userLink.getAttribute('href') : null;
|
|
1264
|
+
const postLink = el.querySelector('a[href*="/p/"], a[href*="/reel/"]');
|
|
1265
|
+
const postUrl = postLink ? 'https://www.instagram.com' + postLink.getAttribute('href') : null;
|
|
1266
|
+
const captionEl = el.querySelector('span._ap3a._aaco._aacu._aacx._aad7._aade');
|
|
1267
|
+
const caption = captionEl?.innerText?.trim() || null;
|
|
1268
|
+
const countSpans = Array.from(el.querySelectorAll('section span')).filter(s => s.children.length === 0 && /^[\\d.,]+\\s*(mil|M|K|B)?$/.test(s.textContent?.trim() || ''));
|
|
1269
|
+
const likes = countSpans[0]?.textContent?.trim() || null;
|
|
1270
|
+
const comments = countSpans[1]?.textContent?.trim() || null;
|
|
1271
|
+
const timeEl = el.querySelector('time');
|
|
1272
|
+
const time = timeEl?.textContent?.trim() || null;
|
|
1273
|
+
const datetime = timeEl?.getAttribute('datetime') || null;
|
|
1274
|
+
const isSponsored = !!Array.from(el.querySelectorAll('span')).find(s => s.textContent?.trim() === 'Patrocinado' || s.textContent?.trim() === 'Sponsored');
|
|
1275
|
+
const hasFollow = !!Array.from(el.querySelectorAll('*')).find(e => (e.textContent?.trim() === 'Seguir' || e.textContent?.trim() === 'Follow') && e.children.length === 0);
|
|
1276
|
+
return { username, profileUrl, postUrl, caption, likes, comments, time, datetime, isSponsored, hasFollow };
|
|
1277
|
+
}).filter(p => p.username || p.caption)
|
|
1278
|
+
`;
|
|
1279
|
+
export const browserExtractIgFeedSchema = {
|
|
1280
|
+
...pagedExtractorFields,
|
|
1281
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1282
|
+
tabId: tabIdField,
|
|
1283
|
+
};
|
|
1284
|
+
export const browserExtractIgFeedDescription = 'Extract visible Instagram feed posts as structured JSON ' +
|
|
1285
|
+
'(username, profileUrl, postUrl, caption, likes, comments, time, datetime, isSponsored, hasFollow). ' +
|
|
1286
|
+
'Navigate to instagram.com/ first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call.';
|
|
1287
|
+
export async function handleBrowserExtractIgFeed(args) {
|
|
1288
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1289
|
+
if ('error' in resolved)
|
|
1290
|
+
return text(resolved.error);
|
|
1291
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, IG_FEED_EXTRACTOR_SCRIPT, {
|
|
1292
|
+
...args, dedupeKeys: ['postUrl'], scrollIfEmpty: true,
|
|
1293
|
+
});
|
|
1294
|
+
if ('error' in res)
|
|
1295
|
+
return text(`Extract failed: ${res.error}`);
|
|
1296
|
+
if (res.items.length === 0) {
|
|
1297
|
+
return emptyExtraction(resolved, 'browser_extract_ig_feed', 'the Instagram home feed', 'No feed posts found. Navigate to instagram.com/ and retry with scrolls:2.');
|
|
1298
|
+
}
|
|
1299
|
+
return text(formatExtraction('post', res));
|
|
1300
|
+
}
|
|
1301
|
+
// ── browser_extract_ig_profile ──
|
|
1302
|
+
const IG_PROFILE_EXTRACTOR_SCRIPT = `
|
|
1303
|
+
(() => {
|
|
1304
|
+
const header = document.querySelector('header');
|
|
1305
|
+
if (!header) return null;
|
|
1306
|
+
const username = header.querySelector('h2')?.textContent?.trim() || header.querySelector('h1')?.textContent?.trim() || null;
|
|
1307
|
+
const nameSpan = header.querySelector('span[dir="auto"]');
|
|
1308
|
+
const displayName = nameSpan?.textContent?.trim() || null;
|
|
1309
|
+
const statTexts = Array.from(header.querySelectorAll('span')).filter(s => {
|
|
1310
|
+
const t = s.textContent?.trim() || '';
|
|
1311
|
+
return t.includes('post') || t.includes('seguidore') || t.includes('seguind') || t.includes('follower') || t.includes('following');
|
|
1312
|
+
});
|
|
1313
|
+
const posts = statTexts.find(s => /post/i.test(s.textContent))?.textContent?.trim() || null;
|
|
1314
|
+
const followers = statTexts.find(s => /seguidore|follower/i.test(s.textContent))?.textContent?.trim() || null;
|
|
1315
|
+
const following = statTexts.find(s => /seguind|following/i.test(s.textContent))?.textContent?.trim() || null;
|
|
1316
|
+
const followerExact = header.querySelector('span[title]')?.getAttribute('title') || null;
|
|
1317
|
+
const bioSpans = Array.from(header.querySelectorAll('span[dir="auto"]')).filter(s => {
|
|
1318
|
+
const t = s.textContent?.trim();
|
|
1319
|
+
return t && t !== username && t !== displayName && t.length > 2 && !/(post|seguidore|seguind|follower|following)/i.test(t);
|
|
1320
|
+
});
|
|
1321
|
+
const bio = bioSpans.map(s => s.textContent?.trim()).join('\\n') || null;
|
|
1322
|
+
const extLink = header.querySelector('a[href*="l.instagram.com"]');
|
|
1323
|
+
const website = extLink?.textContent?.trim() || null;
|
|
1324
|
+
const isVerified = !!header.querySelector('svg[aria-label="Verificado"], svg[aria-label="Verified"]');
|
|
1325
|
+
const gridPosts = Array.from(document.querySelectorAll('a[href*="/p/"], a[href*="/reel/"]')).slice(0, 12).map(a => ({
|
|
1326
|
+
url: 'https://www.instagram.com' + a.getAttribute('href'),
|
|
1327
|
+
alt: a.querySelector('img')?.getAttribute('alt')?.substring(0, 100) || null,
|
|
1328
|
+
isReel: a.getAttribute('href').includes('/reel/'),
|
|
1329
|
+
}));
|
|
1330
|
+
const highlights = Array.from(document.querySelectorAll('a[href*="/stories/highlights/"]')).map(a => ({
|
|
1331
|
+
name: a.textContent?.trim() || null,
|
|
1332
|
+
url: 'https://www.instagram.com' + a.getAttribute('href'),
|
|
1333
|
+
}));
|
|
1334
|
+
return { username, displayName, bio, posts, followers, followerExact, following, isVerified, website, gridPosts, highlights };
|
|
1335
|
+
})()
|
|
1336
|
+
`;
|
|
1337
|
+
export const browserExtractIgProfileSchema = {
|
|
1338
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1339
|
+
tabId: tabIdField,
|
|
1340
|
+
};
|
|
1341
|
+
export const browserExtractIgProfileDescription = 'Extract Instagram profile data as structured JSON ' +
|
|
1342
|
+
'(username, displayName, bio, posts, followers, followerExact, following, isVerified, website, gridPosts, highlights). ' +
|
|
1343
|
+
'Navigate to instagram.com/username/ first. Returns bio, stats, post grid (up to 12), and story highlights.';
|
|
1344
|
+
export async function handleBrowserExtractIgProfile(args) {
|
|
1345
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1346
|
+
if ('error' in resolved)
|
|
1347
|
+
return text(resolved.error);
|
|
1348
|
+
const result = await ni.evaluate(resolved.sessionId, IG_PROFILE_EXTRACTOR_SCRIPT, resolved.tabId);
|
|
1349
|
+
if (result.error)
|
|
1350
|
+
return text(`Extract failed: ${result.error}`);
|
|
1351
|
+
const profile = result.result;
|
|
1352
|
+
if (!profile || !profile.username) {
|
|
1353
|
+
return emptyExtraction(resolved, 'browser_extract_ig_profile', 'an Instagram profile page', 'No profile data found. Navigate to instagram.com/username/ first, then retry.');
|
|
1354
|
+
}
|
|
1355
|
+
return text(`Profile:\n\n${JSON.stringify(profile)}`);
|
|
1356
|
+
}
|
|
1357
|
+
// ── browser_extract_ig_post ──
|
|
1358
|
+
const IG_POST_EXTRACTOR_SCRIPT = `
|
|
1359
|
+
(() => {
|
|
1360
|
+
const main = document.querySelector('main');
|
|
1361
|
+
if (!main) return null;
|
|
1362
|
+
const authorLink = Array.from(main.querySelectorAll('a[role="link"]')).find(a => a.getAttribute('href')?.match(/^\\/[^/]+\\/$/));
|
|
1363
|
+
const author = authorLink?.textContent?.trim() || null;
|
|
1364
|
+
let caption = null;
|
|
1365
|
+
if (authorLink) {
|
|
1366
|
+
const container = authorLink.parentElement?.parentElement;
|
|
1367
|
+
if (container) {
|
|
1368
|
+
const spans = Array.from(container.querySelectorAll('span[dir="auto"]'));
|
|
1369
|
+
caption = spans.map(s => s.textContent?.trim()).filter(t => t && t !== author).join(' ')?.substring(0, 500) || null;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const countSpans = Array.from(main.querySelectorAll('section span')).filter(s => s.children.length === 0 && /^[\\d.,]+\\s*(mil|M|K|B)?$/.test(s.textContent?.trim() || ''));
|
|
1373
|
+
const likes = countSpans.length >= 2 ? countSpans[countSpans.length - 2]?.textContent?.trim() : (countSpans[0]?.textContent?.trim() || null);
|
|
1374
|
+
const commentCount = countSpans.length >= 2 ? countSpans[countSpans.length - 1]?.textContent?.trim() : null;
|
|
1375
|
+
const timeEls = Array.from(main.querySelectorAll('time'));
|
|
1376
|
+
const postTime = timeEls.find(t => t.textContent?.trim()?.startsWith('há') || t.textContent?.trim()?.startsWith('ago') || /^\\d/.test(t.textContent?.trim() || ''));
|
|
1377
|
+
const time = postTime?.textContent?.trim() || timeEls[0]?.textContent?.trim() || null;
|
|
1378
|
+
const datetime = postTime?.getAttribute('datetime') || timeEls[0]?.getAttribute('datetime') || null;
|
|
1379
|
+
const commentLinks = Array.from(main.querySelectorAll('a[href*="/c/"]'));
|
|
1380
|
+
const comments = [];
|
|
1381
|
+
const seen = new Set();
|
|
1382
|
+
for (const link of commentLinks) {
|
|
1383
|
+
const commentUrl = link.getAttribute('href');
|
|
1384
|
+
if (seen.has(commentUrl)) continue;
|
|
1385
|
+
seen.add(commentUrl);
|
|
1386
|
+
const row = link.parentElement?.parentElement?.parentElement;
|
|
1387
|
+
if (!row) continue;
|
|
1388
|
+
const walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT);
|
|
1389
|
+
const texts = [];
|
|
1390
|
+
let node;
|
|
1391
|
+
while (node = walker.nextNode()) {
|
|
1392
|
+
const t = node.textContent?.trim();
|
|
1393
|
+
if (t) texts.push(t);
|
|
1394
|
+
}
|
|
1395
|
+
const cTime = row.querySelector('time');
|
|
1396
|
+
comments.push({
|
|
1397
|
+
username: texts[0] || null,
|
|
1398
|
+
text: texts.slice(2).join(' ')?.substring(0, 200) || texts[1] || null,
|
|
1399
|
+
time: cTime?.textContent?.trim() || null,
|
|
1400
|
+
datetime: cTime?.getAttribute('datetime') || null,
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return { author, caption, likes, commentCount, time, datetime, comments };
|
|
1404
|
+
})()
|
|
1405
|
+
`;
|
|
1406
|
+
export const browserExtractIgPostSchema = {
|
|
1407
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1408
|
+
tabId: tabIdField,
|
|
1409
|
+
};
|
|
1410
|
+
export const browserExtractIgPostDescription = 'Extract a single Instagram post with comments as structured JSON ' +
|
|
1411
|
+
'(author, caption, likes, commentCount, time, datetime, comments[{username, text, time, datetime}]). ' +
|
|
1412
|
+
'Navigate to instagram.com/p/POST_ID/ or instagram.com/reel/REEL_ID/ first. ' +
|
|
1413
|
+
'Scroll the comment area to load more comments before extracting.';
|
|
1414
|
+
export async function handleBrowserExtractIgPost(args) {
|
|
1415
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1416
|
+
if ('error' in resolved)
|
|
1417
|
+
return text(resolved.error);
|
|
1418
|
+
const result = await ni.evaluate(resolved.sessionId, IG_POST_EXTRACTOR_SCRIPT, resolved.tabId);
|
|
1419
|
+
if (result.error)
|
|
1420
|
+
return text(`Extract failed: ${result.error}`);
|
|
1421
|
+
const post = result.result;
|
|
1422
|
+
if (!post || !post.author) {
|
|
1423
|
+
return emptyExtraction(resolved, 'browser_extract_ig_post', 'an Instagram post page', 'No post data found. Navigate to instagram.com/p/POST_ID/ first, then retry.');
|
|
1424
|
+
}
|
|
1425
|
+
return text(`Post:\n\n${JSON.stringify(post)}`);
|
|
1426
|
+
}
|
|
1427
|
+
// ── browser_extract_ig_reels ──
|
|
1428
|
+
const IG_REELS_EXTRACTOR_SCRIPT = `
|
|
1429
|
+
(() => {
|
|
1430
|
+
const main = document.querySelector('main');
|
|
1431
|
+
if (!main) return [];
|
|
1432
|
+
const likeSvgs = Array.from(main.querySelectorAll('svg[aria-label="Curtir"], svg[aria-label="Like"]'));
|
|
1433
|
+
const reels = [];
|
|
1434
|
+
const seen = new Set();
|
|
1435
|
+
for (const svg of likeSvgs) {
|
|
1436
|
+
let container = svg.closest('[role="button"]')?.parentElement;
|
|
1437
|
+
for (let i = 0; i < 5; i++) {
|
|
1438
|
+
if (!container) break;
|
|
1439
|
+
const hasComment = container.querySelector('svg[aria-label="Comentar"], svg[aria-label="Comment"]');
|
|
1440
|
+
const hasShare = container.querySelector('svg[aria-label="Compartilhar"], svg[aria-label="Share"]');
|
|
1441
|
+
if (hasComment && hasShare) break;
|
|
1442
|
+
container = container.parentElement;
|
|
1443
|
+
}
|
|
1444
|
+
if (!container || seen.has(container)) continue;
|
|
1445
|
+
seen.add(container);
|
|
1446
|
+
const userLink = container.querySelector('a[href^="/"]');
|
|
1447
|
+
const spans = Array.from(container.querySelectorAll('span')).filter(s => s.children.length === 0);
|
|
1448
|
+
const counts = spans.filter(s => /^[\\d.,]+\\s*(mil|M|K|B)?$/.test(s.textContent?.trim() || '')).map(s => s.textContent?.trim());
|
|
1449
|
+
const audioLink = container.querySelector('a[href*="/audio/"], a[href*="/music/"]');
|
|
1450
|
+
const captionSpans = spans.filter(s => s.getAttribute('dir') === 'auto' && (s.textContent?.trim().length || 0) > 10);
|
|
1451
|
+
const hasFollow = !!Array.from(container.querySelectorAll('*')).find(e => (e.textContent?.trim() === 'Seguir' || e.textContent?.trim() === 'Follow') && e.children.length === 0);
|
|
1452
|
+
reels.push({
|
|
1453
|
+
username: userLink?.textContent?.trim() || null,
|
|
1454
|
+
profileUrl: userLink ? 'https://www.instagram.com' + userLink.getAttribute('href') : null,
|
|
1455
|
+
caption: captionSpans[0]?.textContent?.trim()?.substring(0, 200) || null,
|
|
1456
|
+
likes: counts[0] || null,
|
|
1457
|
+
comments: counts[1] || null,
|
|
1458
|
+
audioName: audioLink?.textContent?.trim()?.substring(0, 80) || null,
|
|
1459
|
+
audioUrl: audioLink ? 'https://www.instagram.com' + audioLink.getAttribute('href') : null,
|
|
1460
|
+
hasFollow,
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return reels;
|
|
1464
|
+
})()
|
|
1465
|
+
`;
|
|
1466
|
+
export const browserExtractIgReelsSchema = {
|
|
1467
|
+
...pagedExtractorFields,
|
|
1468
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1469
|
+
tabId: tabIdField,
|
|
1470
|
+
};
|
|
1471
|
+
export const browserExtractIgReelsDescription = 'Extract visible Instagram Reels with engagement data as structured JSON ' +
|
|
1472
|
+
'(username, profileUrl, caption, likes, comments, audioName, audioUrl, hasFollow). ' +
|
|
1473
|
+
'Navigate to instagram.com/reels/ first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call.';
|
|
1474
|
+
export async function handleBrowserExtractIgReels(args) {
|
|
1475
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1476
|
+
if ('error' in resolved)
|
|
1477
|
+
return text(resolved.error);
|
|
1478
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, IG_REELS_EXTRACTOR_SCRIPT, {
|
|
1479
|
+
...args, dedupeKeys: ['profileUrl', 'caption'],
|
|
1480
|
+
scrollDistance: 900, // reels advance roughly one viewport per scroll
|
|
1481
|
+
});
|
|
1482
|
+
if ('error' in res)
|
|
1483
|
+
return text(`Extract failed: ${res.error}`);
|
|
1484
|
+
if (res.items.length === 0) {
|
|
1485
|
+
return emptyExtraction(resolved, 'browser_extract_ig_reels', 'the Instagram Reels feed', 'No reels found. Navigate to instagram.com/reels/ and retry with scrolls:2.');
|
|
1486
|
+
}
|
|
1487
|
+
return text(formatExtraction('reel', res));
|
|
1488
|
+
}
|
|
1489
|
+
// ── browser_extract_ig_search ──
|
|
1490
|
+
const IG_SEARCH_EXTRACTOR_SCRIPT = `
|
|
1491
|
+
Array.from(document.querySelectorAll('main a[href*="/p/"], main a[href*="/reel/"]')).map(a => ({
|
|
1492
|
+
url: 'https://www.instagram.com' + a.getAttribute('href'),
|
|
1493
|
+
alt: a.querySelector('img')?.getAttribute('alt')?.substring(0, 120) || null,
|
|
1494
|
+
isReel: a.getAttribute('href').includes('/reel/'),
|
|
1495
|
+
})).filter(p => p.url)
|
|
1496
|
+
`;
|
|
1497
|
+
export const browserExtractIgSearchSchema = {
|
|
1498
|
+
...pagedExtractorFields,
|
|
1499
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1500
|
+
tabId: tabIdField,
|
|
1501
|
+
};
|
|
1502
|
+
export const browserExtractIgSearchDescription = 'Extract posts from the Instagram Explore page as structured JSON (url, alt, isReel). ' +
|
|
1503
|
+
'Navigate to instagram.com/explore/ first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. ' +
|
|
1504
|
+
'For hashtag search, navigate to instagram.com/explore/tags/HASHTAG/.';
|
|
1505
|
+
export async function handleBrowserExtractIgSearch(args) {
|
|
1506
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1507
|
+
if ('error' in resolved)
|
|
1508
|
+
return text(resolved.error);
|
|
1509
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, IG_SEARCH_EXTRACTOR_SCRIPT, {
|
|
1510
|
+
...args, dedupeKeys: ['url'], scrollIfEmpty: true,
|
|
1511
|
+
});
|
|
1512
|
+
if ('error' in res)
|
|
1513
|
+
return text(`Extract failed: ${res.error}`);
|
|
1514
|
+
if (res.items.length === 0) {
|
|
1515
|
+
return emptyExtraction(resolved, 'browser_extract_ig_search', 'the Instagram Explore grid', 'No explore posts found. Navigate to instagram.com/explore/ and retry with scrolls:2.');
|
|
1516
|
+
}
|
|
1517
|
+
return text(formatExtraction('post', res));
|
|
1518
|
+
}
|
|
1519
|
+
// ── LinkedIn Extraction Tools ──
|
|
1520
|
+
// ── browser_extract_li_feed ──
|
|
1521
|
+
const LI_FEED_EXTRACTOR_SCRIPT = `
|
|
1522
|
+
Array.from(document.querySelectorAll('.feed-shared-update-v2[data-urn*="urn:li:activity"]')).map(post => {
|
|
1523
|
+
const actorLink = post.querySelector('.update-components-actor__meta-link') || post.querySelector('.update-components-actor__name a');
|
|
1524
|
+
const actorUrl = actorLink?.href?.split('?')[0] || null;
|
|
1525
|
+
const nameSpan = actorLink?.querySelector('.update-components-actor__title span[aria-hidden="true"]') || actorLink?.querySelector('span[aria-hidden="true"]');
|
|
1526
|
+
const actorName = nameSpan?.textContent?.trim() || null;
|
|
1527
|
+
const descSpan = post.querySelector('.update-components-actor__description span[aria-hidden="true"]');
|
|
1528
|
+
const headline = descSpan?.textContent?.trim() || null;
|
|
1529
|
+
const subDesc = post.querySelector('.update-components-actor__sub-description span[aria-hidden="true"]')?.textContent?.trim() || null;
|
|
1530
|
+
const timeMatch = subDesc?.match(/^(\\d+\\s*\\w+)/) || null;
|
|
1531
|
+
const time = timeMatch ? timeMatch[1] : (subDesc || null);
|
|
1532
|
+
const isPromoted = /Promovido|Promoted/i.test(subDesc || '');
|
|
1533
|
+
const textEl = post.querySelector('.feed-shared-update-v2__description, .update-components-text');
|
|
1534
|
+
const text = textEl?.innerText?.trim() || null;
|
|
1535
|
+
const reactionsBtn = post.querySelector('.social-details-social-counts__reactions-count');
|
|
1536
|
+
const reactions = reactionsBtn?.textContent?.trim() || null;
|
|
1537
|
+
const commentsEl = post.querySelector('.social-details-social-counts__comments');
|
|
1538
|
+
const commentsCount = commentsEl?.textContent?.trim()?.match(/\\d[\\d.,]*/)?.[0] || null;
|
|
1539
|
+
const hasImage = !!post.querySelector('.update-components-image img');
|
|
1540
|
+
const hasVideo = !!post.querySelector('video, .update-components-linkedin-video');
|
|
1541
|
+
const hasArticle = !!post.querySelector('.update-components-article');
|
|
1542
|
+
const hasDocument = !!post.querySelector('.update-components-linkedin-document');
|
|
1543
|
+
const mediaType = hasVideo ? 'video' : hasDocument ? 'document' : hasArticle ? 'article' : hasImage ? 'image' : 'text';
|
|
1544
|
+
const articleTitle = post.querySelector('.update-components-article__title')?.textContent?.trim() || null;
|
|
1545
|
+
const articleLink = post.querySelector('.update-components-article a')?.href?.split('?')[0] || null;
|
|
1546
|
+
const reshareActor = post.querySelector('.update-components-mini-update-v2');
|
|
1547
|
+
const isRepost = !!reshareActor;
|
|
1548
|
+
const postUrn = post.getAttribute('data-urn');
|
|
1549
|
+
const postUrl = postUrn ? 'https://www.linkedin.com/feed/update/' + postUrn + '/' : null;
|
|
1550
|
+
return { author: actorName, authorUrl: actorUrl, headline, time, text, reactions, commentsCount, mediaType, articleTitle, articleLink, isPromoted, isRepost, postUrl };
|
|
1551
|
+
}).filter(p => p.author || p.text)
|
|
1552
|
+
`.trim();
|
|
1553
|
+
export const browserExtractLiFeedSchema = {
|
|
1554
|
+
...pagedExtractorFields,
|
|
1555
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1556
|
+
tabId: tabIdField,
|
|
1557
|
+
};
|
|
1558
|
+
export const browserExtractLiFeedDescription = 'Extract visible LinkedIn feed posts as structured JSON ' +
|
|
1559
|
+
'(author, authorUrl, headline, time, text, reactions, commentsCount, mediaType, articleTitle, articleLink, isPromoted, isRepost, postUrl). ' +
|
|
1560
|
+
'Navigate to linkedin.com/feed/ first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. ' +
|
|
1561
|
+
'Much faster than browser_snapshot for data harvesting from the LinkedIn feed.';
|
|
1562
|
+
export async function handleBrowserExtractLiFeed(args) {
|
|
1563
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1564
|
+
if ('error' in resolved)
|
|
1565
|
+
return text(resolved.error);
|
|
1566
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, LI_FEED_EXTRACTOR_SCRIPT, {
|
|
1567
|
+
...args, dedupeKeys: ['postUrl'], scrollIfEmpty: true,
|
|
1568
|
+
});
|
|
1569
|
+
if ('error' in res)
|
|
1570
|
+
return text(`Extract failed: ${res.error}`);
|
|
1571
|
+
if (res.items.length === 0) {
|
|
1572
|
+
return emptyExtraction(resolved, 'browser_extract_li_feed', 'the LinkedIn home feed', 'No LinkedIn feed posts found. Navigate to linkedin.com/feed/ and retry with scrolls:2.');
|
|
1573
|
+
}
|
|
1574
|
+
return text(formatExtraction('post', res));
|
|
1575
|
+
}
|
|
1576
|
+
// ── browser_extract_li_profile ──
|
|
1577
|
+
const LI_PROFILE_EXTRACTOR_SCRIPT = `
|
|
1578
|
+
(() => {
|
|
1579
|
+
const name = document.querySelector('.text-heading-xlarge, h1')?.textContent?.trim() || null;
|
|
1580
|
+
const headline = document.querySelector('.text-body-medium.break-words')?.textContent?.trim() || null;
|
|
1581
|
+
const location = document.querySelector('.text-body-small.inline.t-black--light.break-words')?.textContent?.trim() || null;
|
|
1582
|
+
const profilePic = document.querySelector('.pv-top-card-profile-picture__image, img.profile-photo-edit__preview')?.src || null;
|
|
1583
|
+
const connectionsEl = Array.from(document.querySelectorAll('span')).find(s => /conexõ|connection/i.test(s.textContent || ''));
|
|
1584
|
+
const connections = connectionsEl?.textContent?.trim() || null;
|
|
1585
|
+
const aboutSection = document.querySelector('.pv-about-section .inline-show-more-text, section .pv-shared-text-with-see-more span[aria-hidden="true"]');
|
|
1586
|
+
const about = aboutSection?.textContent?.trim() || null;
|
|
1587
|
+
const sections = Array.from(document.querySelectorAll('section.artdeco-card')).map(s => {
|
|
1588
|
+
const heading = (s.querySelector('.pvs-header__title span[aria-hidden="true"]') || s.querySelector('h2 span[aria-hidden="true"]') || s.querySelector('h2'))?.textContent?.trim()?.replace(/\\s+/g, ' ');
|
|
1589
|
+
if (!heading) return null;
|
|
1590
|
+
const items = Array.from(s.querySelectorAll('li.artdeco-list__item, li.pvs-list__paged-list-item')).map(li => {
|
|
1591
|
+
const title = li.querySelector('.t-bold span[aria-hidden="true"], .mr1.t-bold span')?.textContent?.trim();
|
|
1592
|
+
const subtitle = li.querySelector('.t-normal span[aria-hidden="true"], .t-14.t-normal span')?.textContent?.trim();
|
|
1593
|
+
const meta = li.querySelector('.pvs-entity__caption-wrapper span[aria-hidden="true"]')?.textContent?.trim();
|
|
1594
|
+
return { title: title || null, subtitle: subtitle || null, meta: meta || null };
|
|
1595
|
+
}).filter(i => i.title);
|
|
1596
|
+
return { heading, items };
|
|
1597
|
+
}).filter(Boolean);
|
|
1598
|
+
const activityPosts = Array.from(document.querySelectorAll('.pv-recent-activity-section li, section .feed-shared-update-v2')).slice(0, 5).map(p => ({
|
|
1599
|
+
text: p.querySelector('.feed-shared-update-v2__description, .update-components-text')?.innerText?.trim()?.substring(0, 200) || p.innerText?.trim()?.substring(0, 200),
|
|
1600
|
+
}));
|
|
1601
|
+
return { name, headline, location, profilePic, connections, about, sections, recentActivity: activityPosts };
|
|
1602
|
+
})()
|
|
1603
|
+
`.trim();
|
|
1604
|
+
export const browserExtractLiProfileSchema = {
|
|
1605
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1606
|
+
tabId: tabIdField,
|
|
1607
|
+
};
|
|
1608
|
+
export const browserExtractLiProfileDescription = 'Extract LinkedIn profile data as structured JSON ' +
|
|
1609
|
+
'(name, headline, location, profilePic, connections, about, sections[{heading, items[{title, subtitle, meta}]}], recentActivity). ' +
|
|
1610
|
+
'Navigate to linkedin.com/in/USERNAME/ first. Sections include Experience, Education, Skills, etc. ' +
|
|
1611
|
+
'Use linkedin.com/in/me/ for the logged-in user\'s own profile.';
|
|
1612
|
+
export async function handleBrowserExtractLiProfile(args) {
|
|
1613
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1614
|
+
if ('error' in resolved)
|
|
1615
|
+
return text(resolved.error);
|
|
1616
|
+
const result = await ni.evaluate(resolved.sessionId, LI_PROFILE_EXTRACTOR_SCRIPT, resolved.tabId);
|
|
1617
|
+
if (result.error)
|
|
1618
|
+
return text(`Extract failed: ${result.error}`);
|
|
1619
|
+
const profile = result.result;
|
|
1620
|
+
if (!profile || !profile.name) {
|
|
1621
|
+
return emptyExtraction(resolved, 'browser_extract_li_profile', 'a LinkedIn profile page', 'No profile data found. Navigate to linkedin.com/in/USERNAME/ first, then retry.');
|
|
1622
|
+
}
|
|
1623
|
+
return text(`Profile:\n\n${JSON.stringify(profile)}`);
|
|
1624
|
+
}
|
|
1625
|
+
// ── browser_extract_li_post ──
|
|
1626
|
+
const LI_POST_EXTRACTOR_SCRIPT = `
|
|
1627
|
+
(() => {
|
|
1628
|
+
const post = document.querySelector('.feed-shared-update-v2[data-urn]');
|
|
1629
|
+
if (!post) return null;
|
|
1630
|
+
const actorLink = post.querySelector('.update-components-actor__meta-link') || post.querySelector('.update-components-actor__name a');
|
|
1631
|
+
const nameSpan = actorLink?.querySelector('.update-components-actor__title span[aria-hidden="true"]') || actorLink?.querySelector('span[aria-hidden="true"]');
|
|
1632
|
+
const author = nameSpan?.textContent?.trim() || null;
|
|
1633
|
+
const authorUrl = actorLink?.href?.split('?')[0] || null;
|
|
1634
|
+
const headline = post.querySelector('.update-components-actor__description span[aria-hidden="true"]')?.textContent?.trim() || null;
|
|
1635
|
+
const subDesc = post.querySelector('.update-components-actor__sub-description span[aria-hidden="true"]')?.textContent?.trim() || null;
|
|
1636
|
+
const text = post.querySelector('.feed-shared-update-v2__description, .update-components-text')?.innerText?.trim() || null;
|
|
1637
|
+
const reactions = post.querySelector('.social-details-social-counts__reactions-count')?.textContent?.trim() || null;
|
|
1638
|
+
const commentsCountEl = post.querySelector('.social-details-social-counts__comments');
|
|
1639
|
+
const commentsCount = commentsCountEl?.textContent?.trim()?.match(/\\d[\\d.,]*/)?.[0] || null;
|
|
1640
|
+
const postUrn = post.getAttribute('data-urn');
|
|
1641
|
+
const comments = Array.from(document.querySelectorAll('.comments-comment-item, article.comments-comment-entity')).map(c => {
|
|
1642
|
+
const cAuthorLink = c.querySelector('a[href*="/in/"]');
|
|
1643
|
+
const cName = c.querySelector('.comments-post-meta__name-text span[aria-hidden="true"]')?.textContent?.trim() ||
|
|
1644
|
+
cAuthorLink?.textContent?.trim()?.replace(/\\s+/g, ' ') || null;
|
|
1645
|
+
const cUrl = cAuthorLink?.href?.split('?')[0] || null;
|
|
1646
|
+
const cText = c.querySelector('.comments-comment-item__main-content, .comments-comment-item-content-body, .feed-shared-inline-show-more-text')?.innerText?.trim() || null;
|
|
1647
|
+
const cTime = c.querySelector('time')?.textContent?.trim() || c.querySelector('.comments-comment-item__timestamp')?.textContent?.trim() || null;
|
|
1648
|
+
const cLikes = c.querySelector('.comments-comment-social-bar__reactions-count, .social-details-social-counts__reactions-count')?.textContent?.trim() || null;
|
|
1649
|
+
return { author: cName, authorUrl: cUrl, text: cText, time: cTime, likes: cLikes };
|
|
1650
|
+
}).filter(c => c.text);
|
|
1651
|
+
return { author, authorUrl, headline, time: subDesc, text, reactions, commentsCount, postUrn, comments };
|
|
1652
|
+
})()
|
|
1653
|
+
`.trim();
|
|
1654
|
+
export const browserExtractLiPostSchema = {
|
|
1655
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1656
|
+
tabId: tabIdField,
|
|
1657
|
+
};
|
|
1658
|
+
export const browserExtractLiPostDescription = 'Extract a single LinkedIn post with comments as structured JSON ' +
|
|
1659
|
+
'(author, authorUrl, headline, time, text, reactions, commentsCount, postUrn, comments[{author, authorUrl, text, time, likes}]). ' +
|
|
1660
|
+
'Navigate to linkedin.com/feed/update/urn:li:activity:ID/ first. ' +
|
|
1661
|
+
'Click "Comentar" button to expand comment section, then scroll to load more comments before extracting.';
|
|
1662
|
+
export async function handleBrowserExtractLiPost(args) {
|
|
1663
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1664
|
+
if ('error' in resolved)
|
|
1665
|
+
return text(resolved.error);
|
|
1666
|
+
const result = await ni.evaluate(resolved.sessionId, LI_POST_EXTRACTOR_SCRIPT, resolved.tabId);
|
|
1667
|
+
if (result.error)
|
|
1668
|
+
return text(`Extract failed: ${result.error}`);
|
|
1669
|
+
const post = result.result;
|
|
1670
|
+
if (!post || !post.author) {
|
|
1671
|
+
return emptyExtraction(resolved, 'browser_extract_li_post', 'a LinkedIn post page', 'No post data found. Navigate to linkedin.com/feed/update/urn:li:activity:ID/ first, then retry.');
|
|
1672
|
+
}
|
|
1673
|
+
return text(`Post:\n\n${JSON.stringify(post)}`);
|
|
1674
|
+
}
|
|
1675
|
+
// ── browser_extract_li_notifications ──
|
|
1676
|
+
const LI_NOTIFICATIONS_EXTRACTOR_SCRIPT = `
|
|
1677
|
+
Array.from(document.querySelectorAll('article.nt-card')).map(card => {
|
|
1678
|
+
const isUnread = card.classList.contains('nt-card--unread');
|
|
1679
|
+
const link = card.querySelector('a[href*="linkedin.com"]');
|
|
1680
|
+
const url = link?.href?.split('&or')[0] || link?.href || null;
|
|
1681
|
+
const allText = card.innerText?.trim()?.replace(/\\s+/g, ' ') || null;
|
|
1682
|
+
const cleanText = allText?.replace(/^(Notificação não lida\\.\\s*|O status está off-line\\s*)/, '')?.replace(/\\s*há\\s*\\d+.*$/, '')?.trim() || allText;
|
|
1683
|
+
const timeEl = Array.from(card.querySelectorAll('span, p')).find(s => /^\\d+\\s*(min|h|d|sem|s|m|dia|hour|day|week|month|mo)/i.test(s.textContent?.trim() || ''));
|
|
1684
|
+
const time = timeEl?.textContent?.trim() || null;
|
|
1685
|
+
const img = card.querySelector('img')?.src || null;
|
|
1686
|
+
return { text: cleanText?.substring(0, 300), time, isUnread, url, image: img };
|
|
1687
|
+
}).filter(n => n.text && !n.text.includes('funciona melhor no novo aplicativo'))
|
|
1688
|
+
`.trim();
|
|
1689
|
+
export const browserExtractLiNotificationsSchema = {
|
|
1690
|
+
...pagedExtractorFields,
|
|
1691
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1692
|
+
tabId: tabIdField,
|
|
1693
|
+
};
|
|
1694
|
+
export const browserExtractLiNotificationsDescription = 'Extract LinkedIn notifications as structured JSON (text, time, isUnread, url, image). ' +
|
|
1695
|
+
'Navigate to linkedin.com/notifications/ first. Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. ' +
|
|
1696
|
+
'Useful for monitoring engagement, connection requests, and mentions.';
|
|
1697
|
+
export async function handleBrowserExtractLiNotifications(args) {
|
|
1698
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1699
|
+
if ('error' in resolved)
|
|
1700
|
+
return text(resolved.error);
|
|
1701
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, LI_NOTIFICATIONS_EXTRACTOR_SCRIPT, {
|
|
1702
|
+
...args, dedupeKeys: ['url', 'text'],
|
|
1703
|
+
});
|
|
1704
|
+
if ('error' in res)
|
|
1705
|
+
return text(`Extract failed: ${res.error}`);
|
|
1706
|
+
if (res.items.length === 0) {
|
|
1707
|
+
return emptyExtraction(resolved, 'browser_extract_li_notifications', 'the LinkedIn notifications page', 'No notifications found. Navigate to linkedin.com/notifications/ first, then retry.');
|
|
1708
|
+
}
|
|
1709
|
+
return text(formatExtraction('notification', res));
|
|
1710
|
+
}
|
|
1711
|
+
// ── browser_extract_li_messages ──
|
|
1712
|
+
const LI_MESSAGES_EXTRACTOR_SCRIPT = `
|
|
1713
|
+
(() => {
|
|
1714
|
+
const results = { conversations: [], activeThread: [] };
|
|
1715
|
+
results.conversations = Array.from(document.querySelectorAll('.msg-conversation-listitem')).map(c => {
|
|
1716
|
+
const name = c.querySelector('.msg-conversation-listitem__participant-names, .msg-conversation-card__participant-names')?.textContent?.trim() || null;
|
|
1717
|
+
const snippet = c.querySelector('.msg-conversation-listitem__message-snippet, .msg-conversation-card__message-snippet')?.textContent?.trim()?.substring(0, 150) || null;
|
|
1718
|
+
const time = c.querySelector('.msg-conversation-listitem__time-stamp, .msg-conversation-card__time-stamp, time')?.textContent?.trim() || null;
|
|
1719
|
+
const isUnread = c.classList.contains('msg-conversation-listitem--unread');
|
|
1720
|
+
const link = c.querySelector('a')?.href || null;
|
|
1721
|
+
return { name, lastMessage: snippet, time, isUnread, url: link };
|
|
1722
|
+
}).filter(c => c.name);
|
|
1723
|
+
results.activeThread = Array.from(document.querySelectorAll('.msg-s-message-list__event, .msg-s-event-listitem')).map(m => {
|
|
1724
|
+
const sender = m.querySelector('.msg-s-message-group__name, .msg-s-event-listitem__link')?.textContent?.trim() || null;
|
|
1725
|
+
const text = m.querySelector('.msg-s-event-listitem__body, .msg-s-event-listitem__message-bubble')?.textContent?.trim()?.substring(0, 500) || null;
|
|
1726
|
+
const time = m.querySelector('.msg-s-message-group__timestamp, time')?.textContent?.trim() || null;
|
|
1727
|
+
const isIncoming = !m.classList.contains('msg-s-message-list__event--outgoing');
|
|
1728
|
+
return { sender, text, time, direction: isIncoming ? 'in' : 'out' };
|
|
1729
|
+
}).filter(m => m.text);
|
|
1730
|
+
return results;
|
|
1731
|
+
})()
|
|
1732
|
+
`.trim();
|
|
1733
|
+
export const browserExtractLiMessagesSchema = {
|
|
1734
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1735
|
+
tabId: tabIdField,
|
|
1736
|
+
};
|
|
1737
|
+
export const browserExtractLiMessagesDescription = 'Extract LinkedIn messaging data as structured JSON with two arrays: ' +
|
|
1738
|
+
'conversations[{name, lastMessage, time, isUnread, url}] (sidebar list) and ' +
|
|
1739
|
+
'activeThread[{sender, text, time, direction}] (open conversation messages). ' +
|
|
1740
|
+
'Navigate to linkedin.com/messaging/ first. Click a conversation to load its messages. ' +
|
|
1741
|
+
'To send a message: browser_fill on .msg-form__contenteditable then browser_click on .msg-form__send-button.';
|
|
1742
|
+
export async function handleBrowserExtractLiMessages(args) {
|
|
1743
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1744
|
+
if ('error' in resolved)
|
|
1745
|
+
return text(resolved.error);
|
|
1746
|
+
const result = await ni.evaluate(resolved.sessionId, LI_MESSAGES_EXTRACTOR_SCRIPT, resolved.tabId);
|
|
1747
|
+
if (result.error)
|
|
1748
|
+
return text(`Extract failed: ${result.error}`);
|
|
1749
|
+
const data = result.result;
|
|
1750
|
+
const convos = data?.conversations || [];
|
|
1751
|
+
const msgs = data?.activeThread || [];
|
|
1752
|
+
if (convos.length === 0 && msgs.length === 0) {
|
|
1753
|
+
return emptyExtraction(resolved, 'browser_extract_li_messages', 'LinkedIn messaging', 'No messages found. Navigate to linkedin.com/messaging/ first, then retry.');
|
|
1754
|
+
}
|
|
1755
|
+
return text(`${convos.length} conversation(s), ${msgs.length} message(s) in active thread:\n\n${JSON.stringify(data)}`);
|
|
1756
|
+
}
|
|
1757
|
+
// ── browser_extract_li_search_people ──
|
|
1758
|
+
const LI_SEARCH_PEOPLE_EXTRACTOR_SCRIPT = `
|
|
1759
|
+
(() => {
|
|
1760
|
+
const main = document.querySelector('main') || document.body;
|
|
1761
|
+
const profileLinks = Array.from(main.querySelectorAll('a[href*="/in/"]')).filter(a => {
|
|
1762
|
+
const href = a.getAttribute('href') || '';
|
|
1763
|
+
return href.match(/\\/in\\/[^/]+\\/?$/) && !href.includes('/search/') && a.textContent?.trim()?.length > 2;
|
|
1764
|
+
});
|
|
1765
|
+
const seen = new Set();
|
|
1766
|
+
return profileLinks.map(a => {
|
|
1767
|
+
const href = a.href?.split('?')[0];
|
|
1768
|
+
if (seen.has(href)) return null;
|
|
1769
|
+
seen.add(href);
|
|
1770
|
+
const card = a.parentElement;
|
|
1771
|
+
if (!card) return null;
|
|
1772
|
+
const pEls = Array.from(card.querySelectorAll('p'));
|
|
1773
|
+
const nameText = a.textContent?.trim()?.replace(/\\s+/g, ' ')?.replace(/\\s*•.*/, '') || null;
|
|
1774
|
+
const headline = pEls[1]?.textContent?.trim() || null;
|
|
1775
|
+
const location = pEls[2]?.textContent?.trim() || null;
|
|
1776
|
+
const currentRole = pEls.find(p => /^(Atual|Current)/i.test(p.textContent?.trim() || ''))?.textContent?.trim() || null;
|
|
1777
|
+
const followers = pEls.find(p => /seguidores|followers/i.test(p.textContent?.trim() || ''))?.textContent?.trim() || null;
|
|
1778
|
+
const mutual = pEls.find(p => /em comum|mutual/i.test(p.textContent?.trim() || ''))?.textContent?.trim() || null;
|
|
1779
|
+
const connectBtn = card.querySelector('button');
|
|
1780
|
+
const btnText = connectBtn?.textContent?.trim();
|
|
1781
|
+
const img = card.querySelector('img')?.src || null;
|
|
1782
|
+
return {
|
|
1783
|
+
name: nameText?.substring(0, 80),
|
|
1784
|
+
profileUrl: href,
|
|
1785
|
+
headline: headline?.substring(0, 200) || null,
|
|
1786
|
+
location: location?.substring(0, 80) || null,
|
|
1787
|
+
currentRole: currentRole?.substring(0, 150) || null,
|
|
1788
|
+
followers: followers?.substring(0, 40) || null,
|
|
1789
|
+
mutual: mutual?.substring(0, 80) || null,
|
|
1790
|
+
actionButton: (btnText && btnText.length < 30) ? btnText : null,
|
|
1791
|
+
image: img,
|
|
1792
|
+
};
|
|
1793
|
+
}).filter(Boolean);
|
|
1794
|
+
})()
|
|
1795
|
+
`.trim();
|
|
1796
|
+
export const browserExtractLiSearchPeopleSchema = {
|
|
1797
|
+
...pagedExtractorFields,
|
|
1798
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1799
|
+
tabId: tabIdField,
|
|
1800
|
+
};
|
|
1801
|
+
export const browserExtractLiSearchPeopleDescription = 'Extract LinkedIn people search results as structured JSON ' +
|
|
1802
|
+
'(name, profileUrl, headline, location, mutual, actionButton, image). ' +
|
|
1803
|
+
'Navigate to linkedin.com/search/results/people/?keywords=QUERY first. ' +
|
|
1804
|
+
'Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call. Also works on linkedin.com/search/results/all/.';
|
|
1805
|
+
export async function handleBrowserExtractLiSearchPeople(args) {
|
|
1806
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1807
|
+
if ('error' in resolved)
|
|
1808
|
+
return text(resolved.error);
|
|
1809
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, LI_SEARCH_PEOPLE_EXTRACTOR_SCRIPT, {
|
|
1810
|
+
...args, dedupeKeys: ['profileUrl'],
|
|
1811
|
+
});
|
|
1812
|
+
if ('error' in res)
|
|
1813
|
+
return text(`Extract failed: ${res.error}`);
|
|
1814
|
+
if (res.items.length === 0) {
|
|
1815
|
+
return emptyExtraction(resolved, 'browser_extract_li_search_people', 'LinkedIn people search results', 'No search results found. Navigate to linkedin.com/search/results/people/?keywords=QUERY first, then retry.');
|
|
1816
|
+
}
|
|
1817
|
+
return text(formatExtraction('result', res));
|
|
1818
|
+
}
|
|
1819
|
+
// ── browser_extract_li_network ──
|
|
1820
|
+
const LI_NETWORK_EXTRACTOR_SCRIPT = `
|
|
1821
|
+
(() => {
|
|
1822
|
+
const results = { invitations: [], suggestions: [] };
|
|
1823
|
+
const invCards = document.querySelectorAll('.invitation-card, [data-view-name="invitation-card"]');
|
|
1824
|
+
results.invitations = Array.from(invCards).map(inv => {
|
|
1825
|
+
const name = inv.querySelector('.invitation-card__name, span[aria-hidden="true"]')?.textContent?.trim() || null;
|
|
1826
|
+
const subtitle = inv.querySelector('.invitation-card__subtitle, .invitation-card__occupation')?.textContent?.trim() || null;
|
|
1827
|
+
const acceptBtn = inv.querySelector('button[aria-label*="Aceitar"], button[aria-label*="Accept"]');
|
|
1828
|
+
const ignoreBtn = inv.querySelector('button[aria-label*="Ignorar"], button[aria-label*="Ignore"]');
|
|
1829
|
+
const profileLink = inv.querySelector('a[href*="/in/"]')?.href?.split('?')[0] || null;
|
|
1830
|
+
return { name, subtitle, profileUrl: profileLink, acceptLabel: acceptBtn?.getAttribute('aria-label') || null, ignoreLabel: ignoreBtn?.getAttribute('aria-label') || null };
|
|
1831
|
+
}).filter(i => i.name);
|
|
1832
|
+
const followBtns = Array.from(document.querySelectorAll('main button')).filter(b => {
|
|
1833
|
+
const t = b.textContent?.trim();
|
|
1834
|
+
return t === 'Seguir' || t === '+ Seguir' || t === 'Follow' || t === '+ Follow' || t === 'Conectar' || t === 'Connect';
|
|
1835
|
+
});
|
|
1836
|
+
const seen = new Set();
|
|
1837
|
+
results.suggestions = followBtns.map(btn => {
|
|
1838
|
+
let card = btn.parentElement;
|
|
1839
|
+
for (let i = 0; i < 8; i++) {
|
|
1840
|
+
if (!card) break;
|
|
1841
|
+
if (card.querySelector('img') && card.querySelector('a[href*="/in/"]')) break;
|
|
1842
|
+
card = card.parentElement;
|
|
1843
|
+
}
|
|
1844
|
+
if (!card) return null;
|
|
1845
|
+
const profileLink = card.querySelector('a[href*="/in/"]');
|
|
1846
|
+
const url = profileLink?.href?.split('?')[0];
|
|
1847
|
+
if (!url || seen.has(url)) return null;
|
|
1848
|
+
seen.add(url);
|
|
1849
|
+
const allText = card.innerText?.trim()?.split('\\n').filter(t => t.trim().length > 1) || [];
|
|
1850
|
+
const name = allText[0]?.trim() || null;
|
|
1851
|
+
const nameClean = name?.replace(/[,\\s]*(Top Voice|Premium|Verificado|Verified).*$/i, '')?.trim();
|
|
1852
|
+
const subtitle = allText.find(t => {
|
|
1853
|
+
const clean = t.trim();
|
|
1854
|
+
return clean !== name && clean !== nameClean && !clean.includes('Seguir') && !clean.includes('Follow') && !clean.includes('Conectar') && !clean.includes('Connect') && !/^\\d/.test(clean) && clean.length > 5;
|
|
1855
|
+
})?.trim() || null;
|
|
1856
|
+
const followersText = allText.find(t => /seguidores|followers/i.test(t))?.trim() || null;
|
|
1857
|
+
const img = card.querySelector('img')?.src || null;
|
|
1858
|
+
return { name, subtitle, followers: followersText, profileUrl: url, action: btn.textContent?.trim(), image: img };
|
|
1859
|
+
}).filter(Boolean);
|
|
1860
|
+
return results;
|
|
1861
|
+
})()
|
|
1862
|
+
`.trim();
|
|
1863
|
+
export const browserExtractLiNetworkSchema = {
|
|
1864
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1865
|
+
tabId: tabIdField,
|
|
1866
|
+
};
|
|
1867
|
+
export const browserExtractLiNetworkDescription = 'Extract LinkedIn My Network data as structured JSON with two arrays: ' +
|
|
1868
|
+
'invitations[{name, subtitle, profileUrl, acceptLabel, ignoreLabel}] (pending connection requests) and ' +
|
|
1869
|
+
'suggestions[{name, subtitle, followers, profileUrl, action, image}] (people/creators to follow or connect). ' +
|
|
1870
|
+
'Navigate to linkedin.com/mynetwork/ first. Scroll down to load more suggestions. ' +
|
|
1871
|
+
'To accept an invitation: browser_click on button with the acceptLabel. ' +
|
|
1872
|
+
'To connect: browser_click on the respective Conectar/Connect button.';
|
|
1873
|
+
export async function handleBrowserExtractLiNetwork(args) {
|
|
1874
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1875
|
+
if ('error' in resolved)
|
|
1876
|
+
return text(resolved.error);
|
|
1877
|
+
const result = await ni.evaluate(resolved.sessionId, LI_NETWORK_EXTRACTOR_SCRIPT, resolved.tabId);
|
|
1878
|
+
if (result.error)
|
|
1879
|
+
return text(`Extract failed: ${result.error}`);
|
|
1880
|
+
const data = result.result;
|
|
1881
|
+
const invitations = data?.invitations || [];
|
|
1882
|
+
const suggestions = data?.suggestions || [];
|
|
1883
|
+
if (invitations.length === 0 && suggestions.length === 0) {
|
|
1884
|
+
return emptyExtraction(resolved, 'browser_extract_li_network', 'the LinkedIn My Network page', 'No network data found. Navigate to linkedin.com/mynetwork/ first, then retry.');
|
|
1885
|
+
}
|
|
1886
|
+
return text(`${invitations.length} invitation(s), ${suggestions.length} suggestion(s):\n\n${JSON.stringify(data)}`);
|
|
1887
|
+
}
|
|
1888
|
+
// ── browser_extract_li_jobs ──
|
|
1889
|
+
const LI_JOBS_EXTRACTOR_SCRIPT = `
|
|
1890
|
+
(() => {
|
|
1891
|
+
const jobs = [];
|
|
1892
|
+
const seen = new Set();
|
|
1893
|
+
|
|
1894
|
+
// --- Search results page (/jobs/search/) ---
|
|
1895
|
+
const searchCards = document.querySelectorAll('[data-occludable-job-id]');
|
|
1896
|
+
searchCards.forEach(card => {
|
|
1897
|
+
const jobId = card.getAttribute('data-occludable-job-id');
|
|
1898
|
+
if (seen.has(jobId)) return;
|
|
1899
|
+
seen.add(jobId);
|
|
1900
|
+
|
|
1901
|
+
const titleLink = card.querySelector('a[href*="/jobs/view/"]');
|
|
1902
|
+
const title = titleLink?.querySelector('span[aria-hidden="true"]')?.textContent?.trim()
|
|
1903
|
+
|| titleLink?.querySelector('strong')?.textContent?.trim() || null;
|
|
1904
|
+
const jobUrl = titleLink?.href?.split('?')[0] || null;
|
|
1905
|
+
|
|
1906
|
+
const company = card.querySelector('.artdeco-entity-lockup__subtitle')?.textContent?.trim()?.replace(/\\s+/g, ' ') || null;
|
|
1907
|
+
const companyLink = card.querySelector('a[href*="/company/"]');
|
|
1908
|
+
const companyUrl = companyLink?.href?.split('?')[0] || null;
|
|
1909
|
+
|
|
1910
|
+
const location = card.querySelector('.artdeco-entity-lockup__caption')?.textContent?.trim()?.replace(/\\s+/g, ' ') || null;
|
|
1911
|
+
|
|
1912
|
+
const logo = card.querySelector('img')?.src || null;
|
|
1913
|
+
const footerText = card.querySelector('.job-card-container__footer-wrapper')?.textContent?.trim()?.replace(/\\s+/g, ' ') || '';
|
|
1914
|
+
const promoted = /Promovida|Promoted/i.test(footerText);
|
|
1915
|
+
const easyApply = /Candidatura simplificada|Easy Apply/i.test(footerText);
|
|
1916
|
+
|
|
1917
|
+
// Salary: sometimes in metadata items
|
|
1918
|
+
const allText = card.innerText || '';
|
|
1919
|
+
const salaryMatch = allText.match(/(?:R\\$|US\\$|\\$)[\\s\\d.,\\/]+(?:por\\s+hr|por\\s+m[eê]s|per\\s+hr|per\\s+month|yr|hr|k)?(?:\\s*-\\s*(?:R\\$|US\\$|\\$)?[\\s\\d.,\\/]+(?:por\\s+hr|por\\s+m[eê]s|per\\s+hr|per\\s+month|yr|hr|k)?)?/i);
|
|
1920
|
+
const salary = salaryMatch ? salaryMatch[0].trim() : null;
|
|
1921
|
+
|
|
1922
|
+
if (title) jobs.push({ jobId, title, company, companyUrl, location, salary, jobUrl, logo, promoted, easyApply, postedDate: null });
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
// --- Homepage (/jobs/) — recommended/top-applicant/easy-apply cards ---
|
|
1926
|
+
if (jobs.length === 0) {
|
|
1927
|
+
const homeLinks = document.querySelectorAll('main a[href*="/jobs/collections/"], main a[href*="/jobs/view/"]');
|
|
1928
|
+
homeLinks.forEach(a => {
|
|
1929
|
+
const href = a.getAttribute('href') || '';
|
|
1930
|
+
if (!href.includes('/jobs/collections/') && !href.includes('/jobs/view/')) return;
|
|
1931
|
+
const pEls = Array.from(a.querySelectorAll('p'));
|
|
1932
|
+
if (pEls.length < 2) return; // skip "View all" links
|
|
1933
|
+
|
|
1934
|
+
// Extract jobId from URL if present
|
|
1935
|
+
const idMatch = href.match(/currentJobId=(\\d+)/) || href.match(/\\/jobs\\/view\\/(\\d+)/);
|
|
1936
|
+
const jobId = idMatch ? idMatch[1] : null;
|
|
1937
|
+
if (jobId && seen.has(jobId)) return;
|
|
1938
|
+
if (jobId) seen.add(jobId);
|
|
1939
|
+
|
|
1940
|
+
// Title: first p, strip "(Vaga verificada)" suffix and duplicated text
|
|
1941
|
+
const rawTitle = pEls[0]?.textContent?.trim() || '';
|
|
1942
|
+
const title = rawTitle
|
|
1943
|
+
.replace(/\\s*\\(Vaga verificada\\).*/i, '')
|
|
1944
|
+
.replace(/\\s*\\(Verified listing\\).*/i, '')
|
|
1945
|
+
.trim() || null;
|
|
1946
|
+
if (!title) return;
|
|
1947
|
+
|
|
1948
|
+
// Parse p elements: filter out dots and noise
|
|
1949
|
+
const texts = pEls.map(p => p.textContent?.trim()).filter(t => t && t !== '\\u2022' && t !== '\\u00b7' && t.length > 1);
|
|
1950
|
+
let company = null;
|
|
1951
|
+
let locationVal = null;
|
|
1952
|
+
let salary = null;
|
|
1953
|
+
let postedDate = null;
|
|
1954
|
+
const promoted = /Promovida|Promoted/i.test(a.innerText || '');
|
|
1955
|
+
const easyApply = /Candidatura simplificada|Easy Apply/i.test(a.innerText || '');
|
|
1956
|
+
|
|
1957
|
+
if (texts.length >= 2) company = texts[1];
|
|
1958
|
+
if (texts.length >= 3) locationVal = texts[2];
|
|
1959
|
+
|
|
1960
|
+
// Find salary in any p
|
|
1961
|
+
for (const t of texts) {
|
|
1962
|
+
const sm = t.match(/(?:R\\$|US\\$|\\$)[\\s\\d.,\\/]+(?:por\\s+hr|por\\s+m[eê]s|per\\s+hr|per\\s+month|yr|hr|k)?(?:\\s*-\\s*(?:R\\$|US\\$|\\$)?[\\s\\d.,\\/]+(?:por\\s+hr|por\\s+m[eê]s|per\\s+hr|per\\s+month|yr|hr|k)?)?/i);
|
|
1963
|
+
if (sm) { salary = sm[0].trim(); break; }
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// Find posted date in any p
|
|
1967
|
+
for (const t of texts) {
|
|
1968
|
+
const dm = t.match(/(?:Anunciada\\s+h[aá]|Posted)\\s+.+/i);
|
|
1969
|
+
if (dm) { postedDate = dm[0].trim(); break; }
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Remove noise from company/location
|
|
1973
|
+
if (/^(Promovida|Promoted|Candidatura|Easy Apply)$/i.test(company || '')) company = null;
|
|
1974
|
+
if (/^(Promovida|Promoted|Candidatura|Easy Apply)$/i.test(locationVal || '')) locationVal = null;
|
|
1975
|
+
|
|
1976
|
+
const jobUrl = jobId ? 'https://www.linkedin.com/jobs/view/' + jobId + '/' : null;
|
|
1977
|
+
const logo = a.querySelector('img')?.src || null;
|
|
1978
|
+
|
|
1979
|
+
jobs.push({ jobId, title, company, companyUrl: null, location: locationVal, salary, jobUrl, logo, promoted, easyApply, postedDate });
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
return jobs;
|
|
1984
|
+
})()
|
|
1985
|
+
`.trim();
|
|
1986
|
+
export const browserExtractLiJobsSchema = {
|
|
1987
|
+
...pagedExtractorFields,
|
|
1988
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
1989
|
+
tabId: tabIdField,
|
|
1990
|
+
};
|
|
1991
|
+
export const browserExtractLiJobsDescription = 'Extract LinkedIn job listings as structured JSON ' +
|
|
1992
|
+
'(jobId, title, company, companyUrl, location, salary, jobUrl, logo, promoted, easyApply, postedDate). ' +
|
|
1993
|
+
'Works on linkedin.com/jobs/ (recommendations) and linkedin.com/jobs/search/?keywords=QUERY&location=LOCATION. ' +
|
|
1994
|
+
'Pass scrolls/minItems to auto-scroll, extract, and dedupe in ONE call.';
|
|
1995
|
+
export async function handleBrowserExtractLiJobs(args) {
|
|
1996
|
+
const resolved = await ni.resolveSession(args.sessionId, undefined, args.tabId);
|
|
1997
|
+
if ('error' in resolved)
|
|
1998
|
+
return text(resolved.error);
|
|
1999
|
+
const res = await runPagedExtractor(resolved.sessionId, resolved.tabId, LI_JOBS_EXTRACTOR_SCRIPT, {
|
|
2000
|
+
...args, dedupeKeys: ['jobId', 'jobUrl'], scrollIfEmpty: true,
|
|
2001
|
+
});
|
|
2002
|
+
if ('error' in res)
|
|
2003
|
+
return text(`Extract failed: ${res.error}`);
|
|
2004
|
+
if (res.items.length === 0) {
|
|
2005
|
+
return emptyExtraction(resolved, 'browser_extract_li_jobs', 'LinkedIn job listings', 'No job listings found. Navigate to linkedin.com/jobs/ or linkedin.com/jobs/search/?keywords=QUERY first, then retry with scrolls:2.');
|
|
2006
|
+
}
|
|
2007
|
+
return text(formatExtraction('job', res));
|
|
2008
|
+
}
|
|
2009
|
+
//# sourceMappingURL=browser-observe.js.map
|