agentlinkops 0.5.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.
Files changed (115) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +24 -0
  3. package/CHANGELOG.md +52 -0
  4. package/LICENSE +190 -0
  5. package/NOTICE +17 -0
  6. package/README.md +202 -0
  7. package/bin/agentlinkops.mjs +4 -0
  8. package/bin/linktrail.mjs +6 -0
  9. package/cli/adapters/csv.js +68 -0
  10. package/cli/adapters/sqlite-crm.js +79 -0
  11. package/cli/adapters/suppliers.js +180 -0
  12. package/cli/agent-setup.js +203 -0
  13. package/cli/agentlinkops.mjs +4 -0
  14. package/cli/args.js +18 -0
  15. package/cli/catalog.js +7483 -0
  16. package/cli/check.js +119 -0
  17. package/cli/citation.js +109 -0
  18. package/cli/client.js +60 -0
  19. package/cli/commands.js +183 -0
  20. package/cli/config.js +104 -0
  21. package/cli/connection.js +90 -0
  22. package/cli/context-mcp.js +193 -0
  23. package/cli/context.js +340 -0
  24. package/cli/delivery.js +67 -0
  25. package/cli/doctor.js +283 -0
  26. package/cli/env.js +66 -0
  27. package/cli/fleet.js +82 -0
  28. package/cli/gsc-browser.js +239 -0
  29. package/cli/import.js +108 -0
  30. package/cli/ledger.js +232 -0
  31. package/cli/linktrail.mjs +6 -0
  32. package/cli/local-result.js +161 -0
  33. package/cli/main.js +523 -0
  34. package/cli/mirror.js +126 -0
  35. package/cli/mix.js +123 -0
  36. package/cli/platforms.js +161 -0
  37. package/cli/receipt-performance.js +64 -0
  38. package/cli/receipts.js +122 -0
  39. package/cli/report.js +158 -0
  40. package/cli/setup-plan.js +211 -0
  41. package/cli/skill.js +39 -0
  42. package/cli/state.js +106 -0
  43. package/cli/status.js +51 -0
  44. package/cli/sync.js +179 -0
  45. package/package.json +59 -0
  46. package/references/cli.md +198 -0
  47. package/references/disavow.md +73 -0
  48. package/references/reports.md +53 -0
  49. package/scripts/agentlinkops.py +651 -0
  50. package/scripts/disavow.py +207 -0
  51. package/scripts/linktrail.py +10 -0
  52. package/scripts/migrations/001_crm.sql +45 -0
  53. package/scripts/migrations/002_cloud_events.sql +13 -0
  54. package/scripts/migrations/003_disavow.sql +20 -0
  55. package/scripts/migrations/004_placement_costs.sql +2 -0
  56. package/scripts/report.py +113 -0
  57. package/shared/agent-clients.js +49 -0
  58. package/shared/observation-event.js +43 -0
  59. package/shared/response-contract.js +59 -0
  60. package/shared/search-connection-contract.js +18 -0
  61. package/skills/agentlinkops-assets/SKILL.md +13 -0
  62. package/skills/agentlinkops-assets/agents/openai.yaml +5 -0
  63. package/skills/agentlinkops-campaigns/SKILL.md +17 -0
  64. package/skills/agentlinkops-campaigns/agents/openai.yaml +12 -0
  65. package/skills/agentlinkops-connect/SKILL.md +33 -0
  66. package/skills/agentlinkops-connect/agents/openai.yaml +12 -0
  67. package/skills/agentlinkops-connect/references/agentlinkops.md +266 -0
  68. package/skills/agentlinkops-crm/SKILL.md +27 -0
  69. package/skills/agentlinkops-crm/agents/openai.yaml +12 -0
  70. package/skills/content-defingerprinting/SKILL.md +29 -0
  71. package/skills/content-defingerprinting/scripts/watermarks/README.md +102 -0
  72. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +26 -0
  73. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +52 -0
  74. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +21 -0
  75. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +96 -0
  76. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +432 -0
  77. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +86 -0
  78. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +460 -0
  79. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +651 -0
  80. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +27 -0
  81. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +212 -0
  82. package/skills/internal-linking-optimizer/SKILL.md +56 -0
  83. package/skills/internal-linking-optimizer/references/source-prompt.md +76 -0
  84. package/skills/pre-publish-review/SKILL.md +179 -0
  85. package/skills/source-cited-content-builder/SKILL.md +56 -0
  86. package/skills/source-cited-content-builder/references/source-prompt.md +19 -0
  87. package/skills/source-cited-humanizer/SKILL.md +20 -0
  88. package/src/citations/adapter.js +141 -0
  89. package/src/citations/contract.js +171 -0
  90. package/src/citations/match.js +120 -0
  91. package/src/citations/runner.js +151 -0
  92. package/src/citations/stats.js +74 -0
  93. package/src/competitors/contract.js +157 -0
  94. package/src/competitors/domain-mix.js +199 -0
  95. package/src/context/ga4.js +652 -0
  96. package/src/context/gsc.js +925 -0
  97. package/src/context/site-profile.js +445 -0
  98. package/src/context/util.js +71 -0
  99. package/src/context-tool-registry.js +27 -0
  100. package/src/corpus/acquire.js +135 -0
  101. package/src/corpus/seeds.js +182 -0
  102. package/src/discovery/contract.js +237 -0
  103. package/src/discovery/import.js +156 -0
  104. package/src/tool-views.js +117 -0
  105. package/src/verifier/fetch.js +303 -0
  106. package/src/verifier/index.js +311 -0
  107. package/src/verifier/observation-kind.js +91 -0
  108. package/src/verifier/parser.js +241 -0
  109. package/src/verifier/readiness.js +90 -0
  110. package/src/verifier/render-evidence.js +57 -0
  111. package/src/verifier/robots.js +85 -0
  112. package/src/verifier/transition.js +129 -0
  113. package/src/verifier/url.js +56 -0
  114. package/templates/SITE.template.md +59 -0
  115. package/templates/campaigns.v1.json +65 -0
@@ -0,0 +1,161 @@
1
+ // Portable, accountless first result and explicit repository adoption. No hosted client.
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { readFile, writeFile, mkdir, open, rename, unlink } from 'node:fs/promises';
5
+ import { dirname, join } from 'node:path';
6
+ import { matchesTarget } from '../src/verifier/url.js';
7
+ import { validatePublicUrl } from '../src/verifier/index.js';
8
+ import { runCheck } from './check.js';
9
+ import { normalizeEntry, readLedger } from './ledger.js';
10
+ import { readState, applyRun } from './state.js';
11
+ import { observationRow, observationKey, readObservations } from './mirror.js';
12
+
13
+ export const LOCAL_RESULT_SCHEMA = 'agentlinkops.local-check.v1';
14
+
15
+ const timestamp = z.string().refine(value => Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value);
16
+ const strings = z.array(z.string());
17
+ const redirectSchema = z.strictObject({ from: z.string(), to: z.string(), status: z.number().int() });
18
+ const robotsSchema = z.strictObject({ allowed: z.boolean().nullable(), reason: z.string().nullable(),
19
+ matchedRule: z.string().nullable(), crawlDelaySeconds: z.number().nullable(), fetched: z.string().nullable(),
20
+ sourceUrl: z.string().optional(), robotsUrl: z.string().optional(), httpStatus: z.number().int().nullable().optional(),
21
+ productToken: z.string().optional(), redirects: z.array(redirectSchema).max(5).optional(), retryAfterSeconds: z.number().optional() });
22
+ const readinessSchema = z.strictObject({ requiresRender: z.boolean(), reason: z.string().nullable(), possibleLoginWall: z.boolean(),
23
+ hasVisiblePassword: z.boolean().optional(), targetInScript: z.boolean().optional(), emptyAppRoot: z.boolean().optional(), emptyProfileRoot: z.boolean().optional() });
24
+ const occurrenceSchema = z.strictObject({ href: z.string(), targetUrl: z.string(), anchor: z.string(), rel: strings,
25
+ context: z.string(), locator: z.strictObject({ line: z.number().int(), column: z.number().int(), offset: z.number().int() }).nullable(), visibility: z.literal('not_rendered') });
26
+ const observationSchema = z.strictObject({
27
+ state: z.enum(['present','absent','unknown','source_unavailable']), reason: z.string(), sourceUrl: z.string(), finalUrl: z.string(), targetUrl: z.string(),
28
+ targetScope: z.enum(['exact','domain','subdomain','path']), httpStatus: z.number().int().nullable(), checkedAt: timestamp,
29
+ occurrences: z.array(occurrenceSchema).max(1000),
30
+ directives: z.strictObject({ meta: z.array(z.strictObject({ name: z.string(), content: z.string() })), xRobotsTag: strings, noindex: z.boolean(), nofollow: z.boolean(), indexingStatus: z.string() }),
31
+ redirects: z.array(redirectSchema),
32
+ evidence: z.strictObject({ method: z.string(), checkerVersion: z.string(), complete: z.boolean(), fetchedAt: timestamp.nullable(),
33
+ bytes: z.number().optional(), contentType: z.string().nullable().optional(), etag: z.string().nullable().optional(), lastModified: z.string().nullable().optional(),
34
+ baseUrl: z.string().optional(), rendered: z.boolean().optional(), sha256: z.string().optional(), parseErrors: strings.optional(),
35
+ readiness: readinessSchema.optional(), renderEligibility: z.enum(['required', 'comparison']).optional() }),
36
+ robots: robotsSchema.optional(), robotsHistory: z.array(robotsSchema).max(6).optional(),
37
+ sourceResponse: z.strictObject({ url: z.string(), httpStatus: z.number().int(), contentType: z.string().max(128).nullable(),
38
+ retryAfterSeconds: z.number().nullable(), challenge: z.boolean() }).optional(),
39
+ expectations: z.strictObject({ expectedAnchor: z.string().nullable(), expectedRel: strings.nullable(), anchorMatches: z.boolean().nullable(), relMatches: z.boolean().nullable(), satisfied: z.boolean() }).optional(),
40
+ linkSignature: z.string().optional(), warnings: strings.optional(), retryAfterSeconds: z.number().optional(),
41
+ });
42
+
43
+ const identity = value => JSON.stringify([value.source, value.target, value.scope ?? 'exact']);
44
+ const resultId = (input, observation) => `lr_${createHash('sha256').update(JSON.stringify({ input, observation })).digest('hex')}`;
45
+ const textAt = async path => { try { return await readFile(path, 'utf8'); } catch (error) { if (error.code === 'ENOENT') return ''; throw error; } };
46
+ const append = (held, row) => held + (held && !held.endsWith('\n') ? '\n' : '') + JSON.stringify(row) + '\n';
47
+
48
+ function normalizedInput(input) {
49
+ const source = validatePublicUrl(input?.source), target = validatePublicUrl(input?.target);
50
+ if (!source.valid || !target.valid) throw new Error(`invalid ${!source.valid ? 'source' : 'target'}: ${!source.valid ? source.reason : target.reason}`);
51
+ const entry = normalizeEntry({ source: source.url, target: target.url, scope: input.scope ?? 'exact', intent: 'wanted' }, { assignId: true });
52
+ return { source: entry.source, target: entry.target, scope: entry.scope };
53
+ }
54
+
55
+ export async function localCheck(input, options = {}) {
56
+ const normalized = normalizedInput(input);
57
+ const [row] = await runCheck([{ id: 'lk_portable', intent: 'wanted', ...normalized }], options);
58
+ const observation = row.result;
59
+ return { schema: LOCAL_RESULT_SCHEMA, result_id: resultId(normalized, observation), input: normalized,
60
+ checked_at: observation.checkedAt, observation, provenance: { execution: 'local', verifier: 'agentlinkops-shared-verifier', raw_html_included: false } };
61
+ }
62
+
63
+ export async function checkToFile(input, file, options = {}) {
64
+ // Reserve before fetching, so a collision does not even spend another publisher request.
65
+ const handle = file ? await open(file, 'wx') : null;
66
+ try {
67
+ const result = await localCheck(input, options);
68
+ if (handle) await handle.writeFile(`${JSON.stringify(result, null, 2)}\n`, 'utf8');
69
+ return result;
70
+ } catch (error) {
71
+ if (handle) await unlink(file).catch(() => {});
72
+ throw error;
73
+ } finally { if (handle) await handle.close(); }
74
+ }
75
+
76
+ export function validateLocalResult(result) {
77
+ if (result?.schema !== LOCAL_RESULT_SCHEMA) throw new Error('unsupported local result schema');
78
+ const input = normalizedInput(result.input), observation = result.observation;
79
+ if (JSON.stringify(result).length > 512 * 1024 || !observationSchema.safeParse(observation).success) throw new Error('invalid portable observation schema');
80
+ if (!observation || !['present', 'absent', 'unknown', 'source_unavailable'].includes(observation.state)
81
+ || typeof observation.checkedAt !== 'string' || !Number.isFinite(Date.parse(observation.checkedAt))
82
+ || !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(observation.checkedAt) || observation.checkedAt !== result.checked_at || !Array.isArray(observation.occurrences)
83
+ || observation.sourceUrl !== input.source || observation.targetUrl !== input.target || observation.targetScope !== input.scope
84
+ || typeof observation.evidence?.complete !== 'boolean') throw new Error('invalid portable observation');
85
+ const fields = new Set(['state','reason','sourceUrl','finalUrl','targetUrl','targetScope','httpStatus','checkedAt','occurrences','directives','redirects','evidence','robots','robotsHistory','sourceResponse','expectations','linkSignature','warnings','retryAfterSeconds']);
86
+ if (Object.keys(observation).some(key => !fields.has(key))) throw new Error('unsupported portable observation field');
87
+ const rejectBytes = value => {
88
+ if (!value || typeof value !== 'object') return;
89
+ for (const [key, child] of Object.entries(value)) {
90
+ if (['html','body','headers'].includes(key.toLowerCase())) throw new Error('portable observations cannot contain raw HTML/body/headers');
91
+ rejectBytes(child);
92
+ }
93
+ };
94
+ rejectBytes(observation);
95
+ if (observation.occurrences.some(item => !item || typeof item.targetUrl !== 'string' || !Array.isArray(item.rel)
96
+ || !matchesTarget(item.targetUrl, input.target, input.scope))) throw new Error('invalid portable occurrence');
97
+ if (observation.state === 'present' && (!observation.evidence.complete || !observation.occurrences.length)) throw new Error('present requires complete matching occurrences');
98
+ if (observation.state === 'absent' && (!observation.evidence.complete || observation.occurrences.length)) throw new Error('absent requires complete evidence without occurrences');
99
+ if (observation.state === 'unknown' && observation.occurrences.length) throw new Error('unknown cannot assert matching occurrences');
100
+ if (observation.state === 'source_unavailable' && (![404,410].includes(observation.httpStatus) || observation.occurrences.length)) throw new Error('source_unavailable requires a dated404/410 observation');
101
+ if (observation.html != null || observation.evidence.html != null || observation.evidence.body != null) throw new Error('portable observations cannot contain raw HTML');
102
+ if (result.result_id !== resultId(input, observation)) throw new Error('local result checksum mismatch');
103
+ return { ...result, input };
104
+ }
105
+
106
+ async function finishAdoption(journalPath, tx, config, afterWrite = async () => {}) {
107
+ if (tx.version !== 1 || !Array.isArray(tx.parts) || tx.parts.length > 3
108
+ || tx.parts.some(part => ![config.paths.ledger, config.paths.observations, config.paths.state].includes(part.path)
109
+ || typeof part.before !== 'string' || typeof part.after !== 'string')) throw new Error('invalid adoption recovery journal');
110
+ for (const part of tx.parts) {
111
+ const held = await textAt(part.path);
112
+ if (held === part.after) continue;
113
+ if (held !== part.before) throw new Error(`adoption recovery conflict at ${part.path}; preserve edits and reconcile ${journalPath}`);
114
+ await mkdir(dirname(part.path), { recursive: true });
115
+ const temporary = `${part.path}.${randomUUID()}.tmp`;
116
+ try { await writeFile(temporary, part.after, 'utf8'); await rename(temporary, part.path); }
117
+ finally { await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
118
+ await afterWrite(part.path);
119
+ }
120
+ await unlink(journalPath);
121
+ }
122
+
123
+ export async function adoptLocalResult(config, supplied, { intent = 'wanted', afterWrite } = {}) {
124
+ if (!['wanted', 'expected'].includes(intent)) throw new Error('adoption intent must be wanted or expected');
125
+ const result = validateLocalResult(supplied);
126
+ if (new Set([config.paths.ledger, config.paths.observations, config.paths.state]).size !== 3) throw new Error('adoption destinations must be distinct');
127
+ const lockPath = `${config.paths.ledger}.lock`;
128
+ const lock = await open(lockPath, 'wx');
129
+ const journalPath = join(config.dir, 'adopt-result-transaction.json');
130
+ try {
131
+ const pending = await textAt(journalPath);
132
+ if (pending) await finishAdoption(journalPath, JSON.parse(pending), config, afterWrite);
133
+ const ledger = await readLedger(config.paths.ledger), observations = await readObservations(config.paths.observations);
134
+ if (ledger.missing || ledger.problems.length || observations.problems.length) throw new Error('adoption requires a valid initialized ledger and observation mirror');
135
+ const matches = ledger.entries.filter(entry => identity(entry) === identity(result.input));
136
+ if (matches.length > 1) throw new Error('ambiguous placement: multiple ledger entries');
137
+ const replay = observations.rows.find(row => row.local_result_id === result.result_id);
138
+ if (replay && !matches.some(entry => entry.id === replay.id)) throw new Error('re-adoption conflicts with current ledger placement');
139
+ if (replay) return { result_id: result.result_id, ledger_id: replay.id, created: false, observation_added: false };
140
+ const entry = matches[0] ?? normalizeEntry({ ...result.input, intent, added: result.checked_at.slice(0, 10) }, { assignId: true });
141
+ const row = { ...observationRow(entry.id, result.observation), local_result_id: result.result_id, imported_from: LOCAL_RESULT_SCHEMA };
142
+ const collision = observations.rows.find(held => observationKey(held) === observationKey(row));
143
+ if (collision) {
144
+ if (JSON.stringify(collision.result) !== JSON.stringify(row.result)) throw new Error('conflicting observation at the same placement and check time');
145
+ return { result_id: result.result_id, ledger_id: entry.id, created: false, observation_added: false };
146
+ }
147
+ const state = await readState(config.paths.state);
148
+ const heldAt = state.entries?.[entry.id]?.last_checked;
149
+ const nextState = heldAt && Date.parse(heldAt) >= Date.parse(row.checked_at) ? state : applyRun(state, [row]);
150
+ const parts = [];
151
+ for (const [path, value] of [[config.paths.ledger, matches[0] ? null : entry], [config.paths.observations, row]]) {
152
+ if (!value) continue;
153
+ const before = await textAt(path);
154
+ parts.push({ path, before, after: append(before, value) });
155
+ }
156
+ if (nextState !== state) parts.push({ path: config.paths.state, before: await textAt(config.paths.state), after: `${JSON.stringify(nextState, null, 1)}\n` });
157
+ await writeFile(journalPath, JSON.stringify({ version: 1, parts }), { flag: 'wx' });
158
+ await finishAdoption(journalPath, { version: 1, parts }, config, afterWrite);
159
+ return { result_id: result.result_id, ledger_id: entry.id, created: !matches[0], observation_added: true };
160
+ } finally { await lock.close(); await unlink(lockPath); }
161
+ }