agentlinkops 0.5.0 → 0.6.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +8 -0
- package/CHANGELOG.md +15 -1
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.md +2 -1
- package/bin/agentlinkops.mjs +0 -0
- package/bin/linktrail.mjs +0 -0
- package/cli/adapters/csv.js +0 -0
- package/cli/adapters/sqlite-crm.js +0 -0
- package/cli/adapters/suppliers.js +0 -0
- package/cli/agent-setup.js +0 -0
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +0 -0
- package/cli/check.js +0 -0
- package/cli/citation.js +124 -2
- package/cli/client.js +1 -1
- package/cli/commands.js +0 -0
- package/cli/config.js +0 -0
- package/cli/connection.js +0 -0
- package/cli/context-mcp.js +0 -0
- package/cli/context.js +0 -0
- package/cli/delivery.js +0 -0
- package/cli/doctor.js +19 -0
- package/cli/env.js +0 -0
- package/cli/fleet.js +0 -0
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +0 -0
- package/cli/ledger.js +0 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +0 -0
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +0 -0
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +0 -0
- package/cli/state.js +0 -0
- package/cli/status.js +0 -0
- package/cli/sync.js +0 -0
- package/package.json +14 -3
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/reports.md +0 -0
- package/scripts/agentlinkops.py +0 -0
- package/scripts/disavow.py +0 -0
- package/scripts/linktrail.py +0 -0
- package/scripts/migrations/001_crm.sql +0 -0
- package/scripts/migrations/002_cloud_events.sql +0 -0
- package/scripts/migrations/003_disavow.sql +0 -0
- package/scripts/migrations/004_placement_costs.sql +0 -0
- package/scripts/report.py +0 -0
- package/shared/agent-clients.js +0 -0
- package/shared/observation-event.js +0 -0
- package/shared/response-contract.js +0 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +0 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +0 -0
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +0 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +0 -0
- package/skills/agentlinkops-crm/SKILL.md +0 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/content-defingerprinting/SKILL.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
- package/skills/internal-linking-optimizer/SKILL.md +0 -0
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
- package/skills/pre-publish-review/SKILL.md +0 -0
- package/skills/source-cited-content-builder/SKILL.md +0 -0
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
- package/skills/source-cited-humanizer/SKILL.md +0 -0
- package/src/citations/adapter.js +173 -0
- package/src/citations/alert.js +53 -0
- package/src/citations/browser/engine.js +267 -0
- package/src/citations/browser/gen/core/providers/_shared/google.js +4 -0
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +15 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +22 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +8 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +70 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +59 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +60 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +18 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +62 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +28 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +39 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +66 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +11 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +16 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +54 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +9 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +40 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +65 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +14 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +45 -0
- package/src/citations/browser/gen/core/providers/index.js +15 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +31 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +84 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +38 -0
- package/src/citations/browser/gen/core/providers/types.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +587 -0
- package/src/citations/browser/gen/lib/browser/display.js +147 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +339 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +121 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +46 -0
- package/src/citations/browser/gen/lib/browser/playwrightCompat.js +321 -0
- package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +251 -0
- package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +71 -0
- package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +54 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +52 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +54 -0
- package/src/citations/browser/gen/lib/input/editor/findSendButton.js +21 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +117 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +101 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +14 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +13 -0
- package/src/citations/browser/gen/lib/input/response/getText.js +10 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +59 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +105 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +41 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +11 -0
- package/src/citations/browser/gen/utils/agent-constants.js +87 -0
- package/src/citations/browser/providers.js +166 -0
- package/src/citations/browser/shims/errors.js +26 -0
- package/src/citations/browser/shims/types.js +6 -0
- package/src/citations/browser/shims/utils.js +32 -0
- package/src/citations/contract.js +19 -3
- package/src/citations/fixtures/dataforseo-aio.json +82 -0
- package/src/citations/match.js +0 -0
- package/src/citations/panel-builder.js +44 -0
- package/src/citations/report.js +165 -0
- package/src/citations/runner.js +43 -16
- package/src/citations/stats.js +63 -0
- package/src/citations/watch.js +85 -0
- package/src/competitors/contract.js +0 -0
- package/src/competitors/domain-mix.js +0 -0
- package/src/context/ga4.js +0 -0
- package/src/context/gsc.js +0 -0
- package/src/context/site-profile.js +0 -0
- package/src/context/util.js +0 -0
- package/src/context-tool-registry.js +0 -0
- package/src/corpus/acquire.js +0 -0
- package/src/corpus/seeds.js +0 -0
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/tool-views.js +0 -0
- package/src/verifier/fetch.js +0 -0
- package/src/verifier/index.js +0 -0
- package/src/verifier/observation-kind.js +0 -0
- package/src/verifier/parser.js +0 -0
- package/src/verifier/readiness.js +0 -0
- package/src/verifier/render-evidence.js +0 -0
- package/src/verifier/robots.js +0 -0
- package/src/verifier/transition.js +0 -0
- package/src/verifier/url.js +0 -0
- package/templates/SITE.template.md +0 -0
- package/templates/campaigns.v1.json +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const PROVIDER_NO_OUTPUT_TIMEOUT_MS = {
|
|
2
|
+
chatgpt: 9e4,
|
|
3
|
+
perplexity: 45e3,
|
|
4
|
+
gemini: 45e3,
|
|
5
|
+
claude: 6e4,
|
|
6
|
+
"ai-overview": 45e3
|
|
7
|
+
};
|
|
8
|
+
const PROVIDER_FORCE_EXIT_STABLE_MS = {
|
|
9
|
+
chatgpt: 45e3,
|
|
10
|
+
perplexity: 3e4,
|
|
11
|
+
gemini: 45e3,
|
|
12
|
+
claude: 45e3,
|
|
13
|
+
"ai-overview": 3e4
|
|
14
|
+
};
|
|
15
|
+
const PROVIDER_EDITOR_SELECTORS = {
|
|
16
|
+
chatgpt: [
|
|
17
|
+
"#prompt-textarea",
|
|
18
|
+
'div#prompt-textarea[contenteditable="true"][role="textbox"]',
|
|
19
|
+
'div.ProseMirror[contenteditable="true"][role="textbox"]',
|
|
20
|
+
'div[contenteditable="true"][role="textbox"][aria-multiline="true"][aria-label="Chat with ChatGPT"]'
|
|
21
|
+
],
|
|
22
|
+
perplexity: [
|
|
23
|
+
"#ask-input",
|
|
24
|
+
'div#ask-input[contenteditable="true"][role="textbox"]',
|
|
25
|
+
'div[role="textbox"][data-lexical-editor="true"]',
|
|
26
|
+
'div[contenteditable="true"][role="textbox"][data-lexical-editor="true"]'
|
|
27
|
+
],
|
|
28
|
+
gemini: [
|
|
29
|
+
'div[aria-label="Enter a prompt for Gemini"]',
|
|
30
|
+
'rich-textarea [contenteditable="true"][role="textbox"]',
|
|
31
|
+
'div[contenteditable="true"][role="textbox"][aria-multiline="true"]'
|
|
32
|
+
],
|
|
33
|
+
claude: [
|
|
34
|
+
'[data-testid="chat-input"]',
|
|
35
|
+
'div[data-testid="chat-input"][contenteditable="true"][role="textbox"]',
|
|
36
|
+
'[data-testid="chat-input"][aria-multiline="true"]'
|
|
37
|
+
],
|
|
38
|
+
"ai-overview": ['textarea[name="q"][role="combobox"]', 'textarea[role="combobox"][aria-label="Search"]']
|
|
39
|
+
};
|
|
40
|
+
const PROVIDER_SUBMIT_BTN_SELECTORS = {
|
|
41
|
+
chatgpt: ['button[data-testid="send-button"]'],
|
|
42
|
+
perplexity: ['button[aria-label*="Submit"]'],
|
|
43
|
+
gemini: ['button[aria-label*="Send"]'],
|
|
44
|
+
claude: ['button[aria-label*="Send"]'],
|
|
45
|
+
"ai-overview": []
|
|
46
|
+
};
|
|
47
|
+
const PROVIDER_MODEL_RESPONSE_SELECTORS = {
|
|
48
|
+
chatgpt: [
|
|
49
|
+
'[data-message-author-role="assistant"]',
|
|
50
|
+
'[data-testid^="conversation-turn"][data-turn="assistant"]'
|
|
51
|
+
],
|
|
52
|
+
perplexity: [
|
|
53
|
+
'div[id^="markdown-content-"]',
|
|
54
|
+
'[id^="markdown-content-"] .prose'
|
|
55
|
+
],
|
|
56
|
+
gemini: ["message-content .markdown"],
|
|
57
|
+
claude: [
|
|
58
|
+
'[data-is-streaming="false"] .standard-markdown',
|
|
59
|
+
".standard-markdown"
|
|
60
|
+
],
|
|
61
|
+
"ai-overview": [
|
|
62
|
+
'[data-container-id="main-col"]'
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
const PROVIDER_RESPONSE_GENERATION_SELECTORS = {
|
|
66
|
+
chatgpt: ['button[data-testid="stop-button"]', 'button[aria-label*="stop" i]'],
|
|
67
|
+
perplexity: ['button[aria-label*="stop" i]'],
|
|
68
|
+
gemini: ['button[aria-label*="stop" i]'],
|
|
69
|
+
claude: ['button[aria-label*="stop" i]'],
|
|
70
|
+
"ai-overview": []
|
|
71
|
+
};
|
|
72
|
+
const RETRYABLE_ERRORS = [
|
|
73
|
+
"ERR_SSL_PROTOCOL_ERROR",
|
|
74
|
+
"ERR_CONNECTION",
|
|
75
|
+
"ERR_TIMED_OUT",
|
|
76
|
+
"ERR_PROXY_CONNECTION_FAILED",
|
|
77
|
+
"Timeout"
|
|
78
|
+
];
|
|
79
|
+
export {
|
|
80
|
+
PROVIDER_EDITOR_SELECTORS,
|
|
81
|
+
PROVIDER_FORCE_EXIT_STABLE_MS,
|
|
82
|
+
PROVIDER_MODEL_RESPONSE_SELECTORS,
|
|
83
|
+
PROVIDER_NO_OUTPUT_TIMEOUT_MS,
|
|
84
|
+
PROVIDER_RESPONSE_GENERATION_SELECTORS,
|
|
85
|
+
PROVIDER_SUBMIT_BTN_SELECTORS,
|
|
86
|
+
RETRYABLE_ERRORS
|
|
87
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// AgentLinkOps-owned browser provider configs: surfaces the vendored oneglanse
|
|
2
|
+
// layer never shipped, written against the same ProviderConfig shape so the
|
|
3
|
+
// engine treats them identically. Each config is probe-proven on the real
|
|
4
|
+
// surface through Camoufox + US residential (receipts: 2026-09-17/18).
|
|
5
|
+
//
|
|
6
|
+
// GROK (grok.com): Cloudflare passes from residential exits; anonymous use
|
|
7
|
+
// grants one full answer per fresh context, then a signup nudge — which the
|
|
8
|
+
// fresh-context-per-sample rule already assumes.
|
|
9
|
+
//
|
|
10
|
+
// COPILOT (copilot.microsoft.com): anonymous composer behind a sign-in modal
|
|
11
|
+
// whose Close control resisted automated dismissal; EXPERIMENTAL until the
|
|
12
|
+
// session-login command (T04) lands.
|
|
13
|
+
//
|
|
14
|
+
// BING (bing.com/search): the generative answer block renders WITH cited
|
|
15
|
+
// outbound links, anonymously, no challenge. Pure URL navigation — the prompt
|
|
16
|
+
// is the URL. The block is found by content signature, never position.
|
|
17
|
+
|
|
18
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
|
|
20
|
+
// Generic answer wait: poll the body until its text stabilizes (the vendored
|
|
21
|
+
// signature-based waiter needs provider constants these surfaces lack).
|
|
22
|
+
async function waitForStableBody(page, { stableMs = 2500, timeoutMs = 90000 } = {}) {
|
|
23
|
+
const deadline = Date.now() + timeoutMs;
|
|
24
|
+
let last = -1;
|
|
25
|
+
let stableSince = null;
|
|
26
|
+
while (Date.now() < deadline) {
|
|
27
|
+
const text = await page.evaluate(() => (document.body.innerText || '').length).catch(() => 0);
|
|
28
|
+
if (text === last && text > 300) {
|
|
29
|
+
if (stableSince && Date.now() - stableSince >= stableMs) return true;
|
|
30
|
+
if (!stableSince) stableSince = Date.now();
|
|
31
|
+
} else {
|
|
32
|
+
stableSince = null;
|
|
33
|
+
last = text;
|
|
34
|
+
}
|
|
35
|
+
await sleep(800);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// One page-context collector for bing: find the generative block by signature
|
|
41
|
+
// (several sentences of prose plus >=2 distinct outbound citation hosts in one
|
|
42
|
+
// container) and return answer text and citation links together. An organic
|
|
43
|
+
// result for a different intent can never masquerade as an AI answer, and a
|
|
44
|
+
// missing block returns empty — which the engine reports as
|
|
45
|
+
// "no AI answer block rendered", an unknown, never a citation verdict.
|
|
46
|
+
const bingCollect = () => {
|
|
47
|
+
const findAiBlock = () => {
|
|
48
|
+
// Only result items that are NOT organic cards (b_algo is a plain organic
|
|
49
|
+
// result; the generative block renders under other classes), never the
|
|
50
|
+
// outer wrapper div whose text begins with page chrome.
|
|
51
|
+
const candidates = document.querySelectorAll('#b_results > li:not(.b_algo)');
|
|
52
|
+
for (const el of candidates) {
|
|
53
|
+
const text = (el.innerText || '').trim();
|
|
54
|
+
if (text.length < 300) continue;
|
|
55
|
+
if (/^(skip to|about \d|accessibility)/i.test(text.slice(0, 24))) continue;
|
|
56
|
+
const sentences = text.split(/[.!?]+\s/).filter((x) => x.trim().length > 30).length;
|
|
57
|
+
if (sentences < 3) continue;
|
|
58
|
+
const hosts = new Set();
|
|
59
|
+
for (const a of el.querySelectorAll('a[href^="http"]')) {
|
|
60
|
+
try {
|
|
61
|
+
const u = new URL(a.href);
|
|
62
|
+
if (/bing\.com|microsoft\.com/.test(u.hostname)) continue;
|
|
63
|
+
hosts.add(u.hostname);
|
|
64
|
+
} catch { /* skip malformed */ }
|
|
65
|
+
}
|
|
66
|
+
if (hosts.size >= 3) return el;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
const block = findAiBlock();
|
|
71
|
+
if (!block) return { answer: '', sources: [] };
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const sources = [];
|
|
74
|
+
for (const a of block.querySelectorAll('a[href^="http"]')) {
|
|
75
|
+
try {
|
|
76
|
+
const u = new URL(a.href);
|
|
77
|
+
if (/bing\.com|microsoft\.com/.test(u.hostname)) continue;
|
|
78
|
+
const key = u.hostname + u.pathname;
|
|
79
|
+
if (seen.has(key)) continue;
|
|
80
|
+
seen.add(key);
|
|
81
|
+
sources.push({ url: a.href, title: (a.textContent || '').trim().slice(0, 200) || null, domain: u.hostname });
|
|
82
|
+
} catch { /* skip malformed */ }
|
|
83
|
+
}
|
|
84
|
+
return { answer: (block.innerText || '').trim(), sources };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// duck.ai in-page chat flow: the surface's own JS solves the VQD challenge,
|
|
88
|
+
// so the engine drives fetch() from inside a real duck.ai page — cookies,
|
|
89
|
+
// headers and challenge all native. Claude Haiku 4.5 serves anonymously.
|
|
90
|
+
const DUCK_MODEL = 'claude-haiku-4-5';
|
|
91
|
+
|
|
92
|
+
export async function duckChat(page, prompt) {
|
|
93
|
+
const result = await page.evaluate(async ({ model, text }) => {
|
|
94
|
+
const j = async (r) => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json().catch(() => ({})); };
|
|
95
|
+
// Status first: same-origin response headers carry the VQD token.
|
|
96
|
+
const statusRes = await fetch('https://duck.ai/duckchat/v1/status', { credentials: 'include' });
|
|
97
|
+
const vqd = statusRes.headers.get('x-vqd-4') || statusRes.headers.get('x-vqd');
|
|
98
|
+
if (!vqd) throw new Error('no vqd token in status response');
|
|
99
|
+
const models = await j(await fetch('https://duck.ai/duckchat/v1/models', { credentials: 'include', headers: { 'x-vqd-4': vqd, 'x-vqd': vqd } }));
|
|
100
|
+
const ids = (models.data ?? []).map((m) => m.id ?? m.model ?? String(m));
|
|
101
|
+
if (!ids.some((id) => String(id).includes('claude'))) {
|
|
102
|
+
throw new Error('no claude model in duck lineup: ' + ids.slice(0, 6).join(','));
|
|
103
|
+
}
|
|
104
|
+
const chatRes = await fetch('https://duck.ai/duckchat/v1/chat', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
credentials: 'include',
|
|
107
|
+
headers: { 'content-type': 'application/json', 'x-vqd-4': vqd, 'x-vqd': vqd },
|
|
108
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: text }], stream: true }),
|
|
109
|
+
});
|
|
110
|
+
if (!chatRes.ok) throw new Error('chat HTTP ' + chatRes.status);
|
|
111
|
+
const reader = chatRes.body.getReader();
|
|
112
|
+
const decoder = new TextDecoder();
|
|
113
|
+
let buf = '';
|
|
114
|
+
let message = '';
|
|
115
|
+
for (;;) {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) break;
|
|
118
|
+
buf += decoder.decode(value, { stream: true });
|
|
119
|
+
const lines = buf.split('\n');
|
|
120
|
+
buf = lines.pop() ?? '';
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const t = line.trim();
|
|
123
|
+
if (!t.startsWith('data:')) continue;
|
|
124
|
+
const payload = t.slice(5).trim();
|
|
125
|
+
if (!payload || payload === '[DONE]') continue;
|
|
126
|
+
try {
|
|
127
|
+
const obj = JSON.parse(payload);
|
|
128
|
+
message += obj.message ?? obj.content ?? '';
|
|
129
|
+
} catch { /* partial json across chunks */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { message, models: ids };
|
|
133
|
+
}, { model: DUCK_MODEL, text: prompt });
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const OWN_PROVIDER_CONFIGS = {
|
|
138
|
+
grok: {
|
|
139
|
+
url: 'https://grok.com/',
|
|
140
|
+
label: 'Grok',
|
|
141
|
+
experimental: false,
|
|
142
|
+
navigateToPrompt: null, // the engine's grok branch owns the flow
|
|
143
|
+
waitForResponse: (page) => waitForStableBody(page),
|
|
144
|
+
extractResponse: (page) => page.evaluate(() => (document.body.innerText || '').trim()),
|
|
145
|
+
extractSources: async () => [], // inline links in text; panel extraction unproven
|
|
146
|
+
},
|
|
147
|
+
copilot: {
|
|
148
|
+
url: 'https://copilot.microsoft.com/',
|
|
149
|
+
label: 'Copilot',
|
|
150
|
+
experimental: true,
|
|
151
|
+
waitForResponse: (page) => waitForStableBody(page),
|
|
152
|
+
extractResponse: (page) => page.evaluate(() => (document.body.innerText || '').trim()),
|
|
153
|
+
extractSources: async () => [],
|
|
154
|
+
},
|
|
155
|
+
bing: {
|
|
156
|
+
url: 'https://www.bing.com/',
|
|
157
|
+
label: 'Bing AI answers',
|
|
158
|
+
experimental: false,
|
|
159
|
+
waitForResponse: async (page) => {
|
|
160
|
+
await sleep(6000);
|
|
161
|
+
await waitForStableBody(page, { stableMs: 1800, timeoutMs: 30000 });
|
|
162
|
+
},
|
|
163
|
+
extractResponse: async (page) => (await page.evaluate(bingCollect)).answer,
|
|
164
|
+
extractSources: async (page) => (await page.evaluate(bingCollect)).sources,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Local shims for the @oneglanse/* runtime imports in the vendored browser
|
|
2
|
+
// layer. Upstream, these live in workspace packages we did not vendor; the
|
|
3
|
+
// behavior below is the minimal honest equivalent. The logger writes to
|
|
4
|
+
// stderr so a --json stdout reader never sees framework chatter.
|
|
5
|
+
export class ExternalServiceError extends Error {
|
|
6
|
+
constructor(service, message, status, meta, cause) {
|
|
7
|
+
super(`${service}: ${message}`);
|
|
8
|
+
this.name = 'ExternalServiceError';
|
|
9
|
+
this.service = service;
|
|
10
|
+
this.status = status ?? null;
|
|
11
|
+
this.meta = meta ?? null;
|
|
12
|
+
if (cause) this.cause = cause;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class NotFoundError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'NotFoundError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function toErrorMessage(error) {
|
|
24
|
+
if (error instanceof Error) return error.message;
|
|
25
|
+
return String(error);
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Shim for @oneglanse/utils runtime exports. The selector/timeout constants
|
|
2
|
+
// are re-exported from the vendored agent-constants module unchanged; logger,
|
|
3
|
+
// getDomain and getFaviconUrls are minimal equivalents.
|
|
4
|
+
export {
|
|
5
|
+
PROVIDER_NO_OUTPUT_TIMEOUT_MS,
|
|
6
|
+
PROVIDER_FORCE_EXIT_STABLE_MS,
|
|
7
|
+
PROVIDER_EDITOR_SELECTORS,
|
|
8
|
+
PROVIDER_SUBMIT_BTN_SELECTORS,
|
|
9
|
+
PROVIDER_MODEL_RESPONSE_SELECTORS,
|
|
10
|
+
PROVIDER_RESPONSE_GENERATION_SELECTORS,
|
|
11
|
+
RETRYABLE_ERRORS,
|
|
12
|
+
} from '../gen/utils/agent-constants.js';
|
|
13
|
+
|
|
14
|
+
const envQuiet = () => Boolean(process.env.AGENTLINKOPS_BROWSER_QUIET);
|
|
15
|
+
|
|
16
|
+
export const logger = {
|
|
17
|
+
log: (...args) => { if (!envQuiet()) console.error('[browser]', ...args); },
|
|
18
|
+
warn: (...args) => { if (!envQuiet()) console.error('[browser:warn]', ...args); },
|
|
19
|
+
error: (...args) => { console.error('[browser:error]', ...args); },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function getDomain(url) {
|
|
23
|
+
try { return new URL(url).hostname.replace(/^www\./, ''); }
|
|
24
|
+
catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getFaviconUrls(url) {
|
|
28
|
+
try {
|
|
29
|
+
const u = new URL(url);
|
|
30
|
+
return [`https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`];
|
|
31
|
+
} catch { return []; }
|
|
32
|
+
}
|
|
@@ -29,7 +29,7 @@ export const CITATION_TARGET_SCOPES = Object.freeze(['domain', 'url']);
|
|
|
29
29
|
|
|
30
30
|
// Engines this version knows. `mock` costs nothing, is deterministic per seed, and
|
|
31
31
|
// exists so every test and every dry-run proves the pipeline without spending money.
|
|
32
|
-
export const CITATION_ENGINES = Object.freeze(['mock', 'perplexity']);
|
|
32
|
+
export const CITATION_ENGINES = Object.freeze(['mock', 'perplexity', 'duck', 'google-aio']);
|
|
33
33
|
|
|
34
34
|
// Token rates used ONLY for the pre-call budget estimate. These are not bills; they
|
|
35
35
|
// are the numbers the hard maxUsd cap is enforced against, so they are deliberately
|
|
@@ -74,7 +74,10 @@ export const citationPromptSchema = z.object({
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
export const citationEngineSpecSchema = z.object({
|
|
77
|
-
|
|
77
|
+
// API engines ('mock', 'perplexity') or browser surfaces ('chatgpt',
|
|
78
|
+
// 'perplexity', 'gemini', 'claude', 'ai-overview' with via:'browser').
|
|
79
|
+
engine: z.string().min(2).max(40),
|
|
80
|
+
via: z.enum(['api', 'browser']).default('api'),
|
|
78
81
|
model: z.string().min(1).max(80).optional(),
|
|
79
82
|
});
|
|
80
83
|
|
|
@@ -89,13 +92,23 @@ export const citationPanelSchema = z.object({
|
|
|
89
92
|
// weights, mentionText, fanOut, costEstimateUsd. This is how a dry run simulates a
|
|
90
93
|
// real panel (and how a test simulates a collapse) without spending anything.
|
|
91
94
|
mockFixtures: z.record(z.string(), z.any()).optional(),
|
|
95
|
+
// Competitor domains: the panel watches them alongside the primary target,
|
|
96
|
+
// each as its own cell identity (never averaged with the target).
|
|
97
|
+
competitors: z.array(z.object({
|
|
98
|
+
domain: z.string().min(3).max(253),
|
|
99
|
+
brand: z.string().min(1).max(100),
|
|
100
|
+
aliases: z.array(z.string().min(1).max(100)).max(12).default([]),
|
|
101
|
+
})).max(10).default([]),
|
|
102
|
+
// Locale pinning: surfaces vary answers by geography. Default en-US.
|
|
103
|
+
locale: z.string().min(2).max(10).default('en-US'),
|
|
92
104
|
note: z.string().max(500).optional(),
|
|
93
105
|
});
|
|
94
106
|
|
|
95
107
|
// The engine identity names HOW a surface was reached, because the same engine two
|
|
96
108
|
// ways is two different measurements and is never averaged (limelitgeo/open's rule).
|
|
97
109
|
export function engineIdentity(spec) {
|
|
98
|
-
const
|
|
110
|
+
const provider = spec.via === 'browser' ? 'web-own-browser' : spec.engine === 'mock' ? 'builtin' : 'api';
|
|
111
|
+
const parts = [spec.engine, provider];
|
|
99
112
|
if (spec.model) parts.push(spec.model);
|
|
100
113
|
return parts.join(':');
|
|
101
114
|
}
|
|
@@ -117,6 +130,9 @@ export const evidenceEnvelopeSchema = z.object({
|
|
|
117
130
|
// evidence snapshots distinct content-addressed objects rather than silent
|
|
118
131
|
// duplicates of each other.
|
|
119
132
|
run_index: z.number().int().nonnegative(),
|
|
133
|
+
// Sibling screenshot of the answer surface, same directory as this envelope;
|
|
134
|
+
// present when the engine captures pixels (browser surfaces always do).
|
|
135
|
+
screenshot_file: z.string().max(120).optional(),
|
|
120
136
|
prompt: z.string().min(1),
|
|
121
137
|
engine_identity: z.string().min(1),
|
|
122
138
|
provider_model_version: z.string().min(1),
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.0.0",
|
|
3
|
+
"status_code": 20000,
|
|
4
|
+
"status_message": "Ok.",
|
|
5
|
+
"cost": 0.0012,
|
|
6
|
+
"tasks_count": 1,
|
|
7
|
+
"tasks_error": 0,
|
|
8
|
+
"tasks": [
|
|
9
|
+
{
|
|
10
|
+
"id": "09211815-1535-0067-0000-7b8e2b4e5c91",
|
|
11
|
+
"status_code": 20000,
|
|
12
|
+
"status_message": "Ok.",
|
|
13
|
+
"cost": 0.0012,
|
|
14
|
+
"result_count": 1,
|
|
15
|
+
"result": [
|
|
16
|
+
{
|
|
17
|
+
"keyword": "what is backlink monitoring",
|
|
18
|
+
"type": "organic",
|
|
19
|
+
"se_domain": "google.com",
|
|
20
|
+
"location_code": 2840,
|
|
21
|
+
"language_code": "en",
|
|
22
|
+
"check_url": "https://www.google.com/search?q=what%20is%20backlink%20monitoring&num=10&hl=en&gl=us&gws_rd=cr",
|
|
23
|
+
"datetime": "2026-09-18 18:15:03 +00:00",
|
|
24
|
+
"item_types": ["ai_overview", "organic", "featured_snippet", "people_also_ask"],
|
|
25
|
+
"se_results_count": 12400000,
|
|
26
|
+
"items": [
|
|
27
|
+
{
|
|
28
|
+
"type": "ai_overview",
|
|
29
|
+
"rank_group": 1,
|
|
30
|
+
"rank_absolute": 1,
|
|
31
|
+
"position": "top",
|
|
32
|
+
"xpath": "/html[1]/body[1]/div[6]/div[2]/div[9]/div[1]/div[2]/div[2]/div[1]/div[1]",
|
|
33
|
+
"text": "Backlink monitoring is the process of regularly tracking and analyzing the links pointing to your website from other sites. It involves checking whether your existing backlinks are still active, identifying new backlinks as they appear, and analyzing the quality and relevance of those links. Effective backlink monitoring helps you maintain a healthy link profile, identify potentially harmful links, and find opportunities for new link building. Common tools for backlink monitoring include Ahrefs, Semrush, and Moz, which provide comprehensive dashboards for tracking your link profile over time.",
|
|
34
|
+
"items": [
|
|
35
|
+
{
|
|
36
|
+
"type": "ai_overview_item",
|
|
37
|
+
"source": "backlinkmonitoring.ai",
|
|
38
|
+
"domain": "backlinkmonitoring.ai",
|
|
39
|
+
"title": "What Is Backlink Monitoring and Why It Matters",
|
|
40
|
+
"url": "https://backlinkmonitoring.ai/blog/what-is-backlink-monitoring",
|
|
41
|
+
"snippet": "Backlink monitoring is the practice of regularly tracking and analyzing inbound links to your website..."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "ai_overview_item",
|
|
45
|
+
"source": "adaptify.ai",
|
|
46
|
+
"domain": "adaptify.ai",
|
|
47
|
+
"title": "What is Backlink Monitoring and Why Your SEO Needs It",
|
|
48
|
+
"url": "https://adaptify.ai/blog/what-is-backlink-monitoring-and-why-your-seo-needs-it",
|
|
49
|
+
"snippet": "Effective backlink monitoring helps you maintain a healthy link profile and identify harmful links..."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "ai_overview_item",
|
|
53
|
+
"source": "lynkdog.com",
|
|
54
|
+
"domain": "lynkdog.com",
|
|
55
|
+
"title": "Backlink Monitoring: A Complete Beginner's Guide",
|
|
56
|
+
"url": "https://lynkdog.com/blog/backlink-monitoring-beginners-guide",
|
|
57
|
+
"snippet": "Backlink monitoring involves checking whether your existing backlinks are still active..."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "ai_overview_item",
|
|
61
|
+
"source": "brand24.com",
|
|
62
|
+
"domain": "brand24.com",
|
|
63
|
+
"title": "Backlinks Monitoring 101: Tools, Tips, and Best Practices",
|
|
64
|
+
"url": "https://brand24.com/blog/backlinks-monitoring-101/",
|
|
65
|
+
"snippet": "Common tools for backlink monitoring include Ahrefs, Semrush, and Moz..."
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "organic",
|
|
71
|
+
"rank_group": 1,
|
|
72
|
+
"rank_absolute": 2,
|
|
73
|
+
"url": "https://backlinko.com/hub/seo/backlinks",
|
|
74
|
+
"domain": "backlinko.com",
|
|
75
|
+
"title": "What Are Backlinks in SEO & Why You Need Them"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
package/src/citations/match.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Guided panel authoring: turn a domain + optional brand into a ready-to-run
|
|
2
|
+
// panel with sensible defaults. The buyer who doesn't want to write JSON
|
|
3
|
+
// gets a starting point they can edit.
|
|
4
|
+
export function buildStarterPanel({ domain, brand, aliases = [], engines = [
|
|
5
|
+
{ engine: 'chatgpt', via: 'browser' },
|
|
6
|
+
{ engine: 'duck', model: 'claude-haiku-4-5' },
|
|
7
|
+
{ engine: 'google-aio' },
|
|
8
|
+
], samples = 10, maxUsd = 1.0, competitors = [] } = {}) {
|
|
9
|
+
if (!domain || !brand) throw new Error('domain and brand are required');
|
|
10
|
+
const prompts = defaultPrompts(domain);
|
|
11
|
+
const targets = [
|
|
12
|
+
{ domain, scope: 'domain', brand, aliases },
|
|
13
|
+
...competitors.map((c) => ({ domain: c.domain, scope: 'domain', brand: c.brand, aliases: c.aliases ?? [] })),
|
|
14
|
+
];
|
|
15
|
+
return {
|
|
16
|
+
schema_version: 1,
|
|
17
|
+
note: `Citation watch for ${brand}`,
|
|
18
|
+
targets,
|
|
19
|
+
prompts,
|
|
20
|
+
engines,
|
|
21
|
+
samples,
|
|
22
|
+
maxUsd,
|
|
23
|
+
competitors: competitors.map((c) => ({ domain: c.domain, brand: c.brand, aliases: c.aliases ?? [] })),
|
|
24
|
+
locale: 'en-US',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Ten sensible starting prompts: category, how-to, comparison, tool-seeking.
|
|
29
|
+
// Replace with GSC-derived panels (DP-0032) once search data is wired.
|
|
30
|
+
function defaultPrompts(domain) {
|
|
31
|
+
const base = domain.split('.')[0];
|
|
32
|
+
return [
|
|
33
|
+
{ id: 'category', text: `what is ${base}` },
|
|
34
|
+
{ id: 'best-tools', text: `best ${base} tools` },
|
|
35
|
+
{ id: 'how-to', text: `how to use ${base}` },
|
|
36
|
+
{ id: 'comparison', text: `${base} vs alternatives` },
|
|
37
|
+
{ id: 'open-source', text: `open source ${base}` },
|
|
38
|
+
{ id: 'pricing', text: `${base} pricing` },
|
|
39
|
+
{ id: 'reviews', text: `${base} reviews` },
|
|
40
|
+
{ id: 'tutorial', text: `${base} tutorial` },
|
|
41
|
+
{ id: 'alternatives', text: `${base} alternatives free` },
|
|
42
|
+
{ id: 'for-beginners', text: `${base} for beginners` },
|
|
43
|
+
];
|
|
44
|
+
}
|