agentlinkops 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.mcp.json +8 -0
  4. package/CHANGELOG.md +15 -1
  5. package/README.md +2 -1
  6. package/cli/citation.js +19 -1
  7. package/cli/client.js +1 -1
  8. package/cli/doctor.js +19 -0
  9. package/package.json +9 -1
  10. package/src/citations/browser/engine.js +196 -0
  11. package/src/citations/browser/gen/core/providers/_shared/google.js +4 -0
  12. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +15 -0
  13. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +22 -0
  14. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +8 -0
  15. package/src/citations/browser/gen/core/providers/ai-overview/index.js +70 -0
  16. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +59 -0
  17. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +60 -0
  18. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +18 -0
  19. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +62 -0
  20. package/src/citations/browser/gen/core/providers/chatgpt/index.js +28 -0
  21. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +39 -0
  22. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +66 -0
  23. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +11 -0
  24. package/src/citations/browser/gen/core/providers/claude/index.js +16 -0
  25. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +54 -0
  26. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +9 -0
  27. package/src/citations/browser/gen/core/providers/gemini/index.js +40 -0
  28. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +65 -0
  29. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +14 -0
  30. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +45 -0
  31. package/src/citations/browser/gen/core/providers/index.js +15 -0
  32. package/src/citations/browser/gen/core/providers/perplexity/index.js +31 -0
  33. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +84 -0
  34. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +38 -0
  35. package/src/citations/browser/gen/core/providers/types.js +0 -0
  36. package/src/citations/browser/gen/lib/browser/camoufox.js +587 -0
  37. package/src/citations/browser/gen/lib/browser/display.js +147 -0
  38. package/src/citations/browser/gen/lib/browser/domOps.js +339 -0
  39. package/src/citations/browser/gen/lib/browser/humanBehavior.js +121 -0
  40. package/src/citations/browser/gen/lib/browser/navigate.js +46 -0
  41. package/src/citations/browser/gen/lib/browser/playwrightCompat.js +321 -0
  42. package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +251 -0
  43. package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
  44. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +71 -0
  45. package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +54 -0
  46. package/src/citations/browser/gen/lib/input/editor/clearInput.js +52 -0
  47. package/src/citations/browser/gen/lib/input/editor/findEditor.js +54 -0
  48. package/src/citations/browser/gen/lib/input/editor/findSendButton.js +21 -0
  49. package/src/citations/browser/gen/lib/input/editor/promptInput.js +117 -0
  50. package/src/citations/browser/gen/lib/input/markdown/converter.js +101 -0
  51. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +14 -0
  52. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +13 -0
  53. package/src/citations/browser/gen/lib/input/response/getText.js +10 -0
  54. package/src/citations/browser/gen/lib/input/response/isGenerating.js +59 -0
  55. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +105 -0
  56. package/src/citations/browser/gen/lib/input/sources/findButton.js +41 -0
  57. package/src/citations/browser/gen/lib/input/sources/openPanel.js +11 -0
  58. package/src/citations/browser/gen/utils/agent-constants.js +87 -0
  59. package/src/citations/browser/shims/errors.js +26 -0
  60. package/src/citations/browser/shims/types.js +6 -0
  61. package/src/citations/browser/shims/utils.js +32 -0
  62. package/src/citations/contract.js +9 -2
  63. package/src/citations/runner.js +21 -5
@@ -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,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,6 @@
1
+ // Shim for @oneglanse/types runtime exports used outside the excluded
2
+ // orchestration files. resolveAppMode upstream picks self-host vs cloud app
3
+ // modes; we always run the local (self-host) shape.
4
+ export function resolveAppMode() {
5
+ return 'selfhost';
6
+ }
@@ -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
+ }
@@ -74,7 +74,10 @@ export const citationPromptSchema = z.object({
74
74
  });
75
75
 
76
76
  export const citationEngineSpecSchema = z.object({
77
- engine: z.enum(CITATION_ENGINES),
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
 
@@ -95,7 +98,8 @@ export const citationPanelSchema = z.object({
95
98
  // The engine identity names HOW a surface was reached, because the same engine two
96
99
  // ways is two different measurements and is never averaged (limelitgeo/open's rule).
97
100
  export function engineIdentity(spec) {
98
- const parts = [spec.engine, spec.engine === 'mock' ? 'builtin' : 'api'];
101
+ const provider = spec.via === 'browser' ? 'web-own-browser' : spec.engine === 'mock' ? 'builtin' : 'api';
102
+ const parts = [spec.engine, provider];
99
103
  if (spec.model) parts.push(spec.model);
100
104
  return parts.join(':');
101
105
  }
@@ -117,6 +121,9 @@ export const evidenceEnvelopeSchema = z.object({
117
121
  // evidence snapshots distinct content-addressed objects rather than silent
118
122
  // duplicates of each other.
119
123
  run_index: z.number().int().nonnegative(),
124
+ // Sibling screenshot of the answer surface, same directory as this envelope;
125
+ // present when the engine captures pixels (browser surfaces always do).
126
+ screenshot_file: z.string().max(120).optional(),
120
127
  prompt: z.string().min(1),
121
128
  engine_identity: z.string().min(1),
122
129
  provider_model_version: z.string().min(1),
@@ -94,6 +94,9 @@ export async function runEpoch(panelInput, options = {}) {
94
94
  run = await cell.engine.run({ prompt: cell.prompt.text, runIndex });
95
95
  } catch (cause) {
96
96
  if (cause instanceof EngineError && cause.retriable) {
97
+ // An unknown is honest, but never silent: the reason travels to
98
+ // stderr so a run of unknowns names its wall.
99
+ if (options.err) options.err(`unknown observation (${cell.id}): ${cause.message}`);
97
100
  outcomesByCell.get(cell.id).push('unknown');
98
101
  continue;
99
102
  }
@@ -101,7 +104,7 @@ export async function runEpoch(panelInput, options = {}) {
101
104
  }
102
105
  spent += run.costEstimateUsd ?? 0;
103
106
 
104
- const envelope = evidenceEnvelopeSchema.parse({
107
+ const envelope = {
105
108
  schema_version: CITATIONS_VERSION,
106
109
  epoch_id: epochId, cell_id: cell.id, run_index: runIndex, prompt: cell.prompt.text,
107
110
  engine_identity: cell.identity,
@@ -110,10 +113,17 @@ export async function runEpoch(panelInput, options = {}) {
110
113
  usage: run.usage ?? { input_tokens: 0, output_tokens: 0 },
111
114
  cost_estimate_usd: run.costEstimateUsd ?? 0,
112
115
  at: nowIso(),
113
- });
114
- const evidenceJson = JSON.stringify(envelope);
115
- const digest = sha256(evidenceJson);
116
- await writeFile(join(evidenceRoot, `${digest}.json`), evidenceJson, 'utf8');
116
+ };
117
+ const digest = sha256(JSON.stringify(envelope));
118
+ // The screenshot is a sibling of the envelope: same content-addressed
119
+ // directory, named by the envelope digest it belongs to. An observation
120
+ // of a rendered surface carries the pixels of that surface.
121
+ if (run.screenshotPng) {
122
+ const shotName = `${digest}.screenshot.png`;
123
+ await writeFile(join(evidenceRoot, shotName), run.screenshotPng);
124
+ envelope.screenshot_file = shotName;
125
+ }
126
+ await writeFile(join(evidenceRoot, `${digest}.json`), JSON.stringify(evidenceEnvelopeSchema.parse(envelope)), 'utf8');
117
127
 
118
128
  const tiered = classifyOutcome(cell.target, run, options.verifyFetch ?? null);
119
129
  outcomesByCell.get(cell.id).push(tiered.outcome);
@@ -147,5 +157,11 @@ export async function runEpoch(panelInput, options = {}) {
147
157
  await writeFile(join(dir, CITATION_FILES.observations), observationRows.map((r) => JSON.stringify(r)).join('\n') + (observationRows.length ? '\n' : ''), { flag: 'a' });
148
158
  await writeFile(join(dir, CITATION_FILES.epochs), epochRows.map((r) => JSON.stringify(r)).join('\n') + (epochRows.length ? '\n' : ''), { flag: 'a' });
149
159
 
160
+ // Browser engines hold a browser and maybe an Xvfb display for the whole
161
+ // epoch; release both once the tallies are written.
162
+ for (const engine of new Set(cells.map((c) => c.engine))) {
163
+ try { await engine.close?.(); } catch { /* cleanup is best-effort */ }
164
+ }
165
+
150
166
  return { epochId, cells: epochRows, spentEstimateUsd: Number(spent.toFixed(6)), aborted, samples, maxUsd };
151
167
  }