agentlinkops 0.6.1 → 0.6.3
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 +0 -0
- package/CHANGELOG.md +17 -0
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.md +0 -0
- 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/admission.js +130 -0
- package/cli/agent-setup.js +1014 -101
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +12837 -4694
- package/cli/check.js +0 -0
- package/cli/citation.js +181 -32
- package/cli/client.js +1 -1
- package/cli/commands.js +3 -3
- package/cli/config.js +2 -1
- 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/disavow.js +54 -0
- package/cli/doctor.js +7 -8
- 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/index-observations.js +89 -0
- package/cli/ledger.js +7 -1
- package/cli/lifecycle.js +40 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +99 -7
- 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 +79 -6
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +41 -4
- package/cli/state.js +0 -0
- package/cli/status.js +0 -0
- package/cli/sync.js +23 -1
- package/package.json +3 -2
- package/references/citation-panels.md +26 -0
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/discovery-handoff.md +32 -0
- package/references/recipes/catalog.json +148 -0
- package/references/recipes/command-notes.md +9 -0
- package/references/recipes/placement-reconciliation.md +41 -0
- package/references/recipes/qualified-campaign-handoff.md +56 -0
- package/references/recipes/site-context-brief.md +25 -0
- package/references/recipes/sourced-linkable-asset.md +25 -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/admission-contract.js +16 -0
- package/shared/admission-report-contract.js +8 -0
- package/shared/agent-clients.js +0 -0
- package/shared/citation-evidence-contract.js +27 -0
- package/shared/disavow-contract.js +18 -0
- package/shared/index-observation-contract.js +14 -0
- package/shared/lifecycle-contract.js +22 -0
- package/shared/lifecycle-money.js +24 -0
- package/shared/observation-event.js +12 -1
- package/shared/opportunity-record.js +259 -0
- package/shared/response-contract.js +2 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +2 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +5 -1
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +8 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +9 -5
- package/skills/agentlinkops-crm/SKILL.md +2 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/agentlinkops-discovery/SKILL.md +17 -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 +102 -21
- package/src/citations/alert.js +38 -13
- package/src/citations/browser/engine.js +145 -50
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
- package/src/citations/browser/gen/lib/browser/display.js +0 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
- package/src/citations/browser/gen/utils/agent-constants.js +0 -0
- package/src/citations/browser/providers.js +3 -1
- package/src/citations/browser/shims/errors.js +0 -0
- package/src/citations/browser/shims/types.js +0 -0
- package/src/citations/browser/shims/utils.js +1 -0
- package/src/citations/contract.js +55 -9
- package/src/citations/inventory.js +106 -0
- package/src/citations/login.js +122 -0
- package/src/citations/match.js +12 -0
- package/src/citations/panel-builder.js +44 -39
- package/src/citations/report.js +88 -21
- package/src/citations/runner.js +59 -31
- package/src/citations/stats.js +133 -61
- package/src/citations/sync.js +131 -0
- package/src/citations/watch.js +57 -77
- 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 +47 -8
- package/src/corpus/seeds.js +132 -19
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/evidence-reference.js +112 -0
- package/src/index-observation.js +161 -0
- package/src/locator.js +112 -0
- package/src/tool-views.js +6 -2
- package/src/verifier/fetch.js +42 -6
- 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
- package/src/citations/browser/gen/core/providers/_shared/google.js +0 -4
- package/src/citations/browser/gen/core/providers/types.js +0 -0
- package/src/citations/browser/gen/lib/browser/playwrightCompat.js +0 -321
- package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +0 -251
- package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +0 -54
- package/src/citations/browser/gen/lib/input/editor/findSendButton.js +0 -21
- package/src/citations/browser/gen/lib/input/response/getText.js +0 -10
- package/src/citations/fixtures/dataforseo-aio.json +0 -82
package/cli/lifecycle.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {legacyCostPatch} from '../shared/lifecycle-money.js';
|
|
2
|
+
import {readFile,stat} from 'node:fs/promises';
|
|
3
|
+
import {resolve} from 'node:path';
|
|
4
|
+
import {loadConfig,ConfigError} from './config.js';
|
|
5
|
+
import {cloudConnection} from './connection.js';
|
|
6
|
+
import {createClient} from './client.js';
|
|
7
|
+
import * as C from '../shared/lifecycle-contract.js';
|
|
8
|
+
export const LIFECYCLE_CLI_HELP=`agentlinkops lifecycle get WATCH_ID
|
|
9
|
+
agentlinkops lifecycle update WATCH_ID --revision N --deal PATCH.json [--decimal-cost]
|
|
10
|
+
agentlinkops lifecycle clear WATCH_ID --revision N
|
|
11
|
+
agentlinkops lifecycle renew [--limit 100]
|
|
12
|
+
agentlinkops lifecycle report [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
13
|
+
Uses the connected project. Deal costs are integer currency minor units. Null clears a field; costMinor and currency must be set or cleared together. Clear removes deal metadata, not the watch. Renew retains due events without sending a message. --decimal-cost reads cost_amount as an exact decimal string and cost_currency using the supported currency scale. sync --lifecycle pulls a deal mirror without editing the ledger.`;
|
|
14
|
+
const fail=message=>{throw new ConfigError(message);};
|
|
15
|
+
const integer=(value,name)=>{if(typeof value!=='string'||!/^\d+$/.test(value)||!Number.isSafeInteger(Number(value)))fail(`${name} requires a nonnegative integer`);return Number(value);};
|
|
16
|
+
export async function lifecycleMain(args,{cwd=process.cwd(),env=process.env,out=console.log,fetchImpl=globalThis.fetch}={}){
|
|
17
|
+
if(args.help){out(LIFECYCLE_CLI_HELP);return 0;}
|
|
18
|
+
const action=args._?.[1],allowed={get:[],update:['revision','deal','decimal-cost'],clear:['revision'],renew:['limit'],report:['from','to']}[action];
|
|
19
|
+
if(!allowed||args.tag?.length||Object.keys(args).some(key=>!['_','tag',...allowed].includes(key)))fail(LIFECYCLE_CLI_HELP);
|
|
20
|
+
const record=['get','update','clear'].includes(action);
|
|
21
|
+
if(args._.length!==(record?3:2)||record&&(typeof args._[2]!=='string'||!args._[2]))fail(LIFECYCLE_CLI_HELP);
|
|
22
|
+
const config=await loadConfig({cwd}),connection=cloudConnection(config,env);if(!connection.projectId)fail('Connect this repository to a project first');
|
|
23
|
+
const input={projectId:connection.projectId};let name,schema,output;
|
|
24
|
+
if(record)input.watchId=args._[2];
|
|
25
|
+
if(action==='get'){name='get_link_lifecycle';schema=C.lifecycleGetInput;output=C.lifecycleOutput;}
|
|
26
|
+
if(action==='update'||action==='clear'){
|
|
27
|
+
name='update_link_lifecycle';schema=C.lifecycleUpdateInput;output=C.lifecycleOutput;input.revision=integer(args.revision,'--revision');
|
|
28
|
+
if(action==='clear')input.deal=C.emptyDeal();else{
|
|
29
|
+
if(typeof args.deal!=='string')fail('--deal requires a JSON patch file');const path=resolve(cwd,args.deal),info=await stat(path);if(!info.isFile()||info.size>16384)fail('Deal patch must be a JSON file of at most 16 KiB');
|
|
30
|
+
try{input.deal=JSON.parse(await readFile(path,'utf8'));}catch{fail('Deal patch must contain valid JSON');}
|
|
31
|
+
if(args['decimal-cost']){try{input.deal=legacyCostPatch(input.deal);}catch(e){fail(e.message);}}
|
|
32
|
+
const patch=C.lifecyclePatch.safeParse(input.deal);if(!patch.success)fail('Invalid deal patch fields');
|
|
33
|
+
if(('costMinor' in input.deal)!==('currency' in input.deal))fail('Set or clear costMinor and currency together');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if(action==='renew'){name='record_lifecycle_renewals';schema=C.lifecycleRenewInput;output=C.lifecycleRenewOutput;if(args.limit!==undefined)input.limit=integer(args.limit,'--limit');}
|
|
37
|
+
if(action==='report'){name='get_lifecycle_report';schema=C.lifecycleReportInput;output=C.lifecycleReportOutput;for(const key of ['from','to'])if(args[key]!==undefined)input[key]=args[key];}
|
|
38
|
+
const parsed=schema.safeParse(input);if(!parsed.success)fail('Invalid lifecycle dates, fields or bounds');
|
|
39
|
+
const result=await createClient({...connection,fetchImpl}).callCommand(name,parsed.data),checked=output.safeParse(result);if(!checked.success)fail('Invalid lifecycle service response');out(JSON.stringify(checked.data,null,2));return 0;
|
|
40
|
+
}
|
package/cli/linktrail.mjs
CHANGED
|
File without changes
|
package/cli/local-result.js
CHANGED
|
File without changes
|
package/cli/main.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import {lifecycleMain} from './lifecycle.js';
|
|
2
|
+
import {admissionMain} from './admission.js';
|
|
1
3
|
import { commandsMain } from './commands.js';
|
|
4
|
+
import { disavowMain } from './disavow.js';
|
|
2
5
|
import { agentMain } from './agent-setup.js';
|
|
3
6
|
import { setupPlanMain } from './setup-plan.js';
|
|
4
7
|
import {parseArgs as parseArguments} from './args.js';
|
|
@@ -19,7 +22,7 @@ import { readImport, parseMap } from './import.js';
|
|
|
19
22
|
import { freezeDataset, renderReport, datasetDigest } from './report.js';
|
|
20
23
|
import { createClient, CloudError } from './client.js';
|
|
21
24
|
import { cloudConnection, connectMain, keySetup } from './connection.js';
|
|
22
|
-
import { syncPlan, pushExpectations, pullEvents, cloudObservationRows, watchIndex, pullTargetEvents } from './sync.js';
|
|
25
|
+
import { syncPlan, pushExpectations, pullLifecycleMirrors, pullEvents, cloudObservationRows, watchIndex, pullTargetEvents } from './sync.js';
|
|
23
26
|
import { SUPPLIER_NAMES } from './adapters/suppliers.js';
|
|
24
27
|
import { fleetSummary, renderFleet } from './fleet.js';
|
|
25
28
|
import { platformAggregate, classifyObservation, renderPlatforms, attachToCandidates, PLATFORM_CLASSES } from './platforms.js';
|
|
@@ -30,14 +33,17 @@ import { attachReceiptPerformance } from './receipt-performance.js';
|
|
|
30
33
|
import { contextPaths, loadContextConfig, readGscRows } from '../src/context/gsc.js';
|
|
31
34
|
import { readGa4Rows } from '../src/context/ga4.js';
|
|
32
35
|
import { mixMain } from './mix.js';
|
|
36
|
+
import { locateOccurrences } from '../src/locator.js';
|
|
37
|
+
import { mirrorEvidenceReference, parseEvidenceReference, describeEvidenceReference } from '../src/evidence-reference.js';
|
|
33
38
|
import { doctorMain } from './doctor.js';
|
|
34
39
|
import { gscLinksMain } from './gsc-browser.js';
|
|
35
40
|
import { skillMain, NUDGE } from './skill.js';
|
|
41
|
+
import { indexMain, readIndexReceipts, locateIndexReceipt } from './index-observations.js';
|
|
36
42
|
import { citationMain } from './citation.js';
|
|
37
43
|
|
|
38
44
|
const USAGE = `agentlinkops — a backlink ledger that lives in your repository
|
|
39
45
|
|
|
40
|
-
agentlinkops skill [--url]
|
|
46
|
+
agentlinkops skill [--url|--list [--json]|NAME] read the agent reference or a named skill
|
|
41
47
|
before the first AgentLinkOps call
|
|
42
48
|
agentlinkops init create .agentlinkops/ here
|
|
43
49
|
agentlinkops migrate rename an existing .linktrail/ to .agentlinkops/ (receipt; never merges)
|
|
@@ -45,6 +51,12 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
|
|
|
45
51
|
[--mode local|hosted|external] read-only setup plan; no account required
|
|
46
52
|
agentlinkops add --source URL --target URL [--intent wanted|expected] [--scope …]
|
|
47
53
|
[--anchor TEXT] [--rel a,b] [--ref TEXT] [--tag t --tag t] [--note TEXT]
|
|
54
|
+
agentlinkops index pull --watch-id ID [--limit N] [--before ID]
|
|
55
|
+
agentlinkops lifecycle get|update|clear|renew|report
|
|
56
|
+
agentlinkops admission list|add|update|remove|evaluate|reevaluate|receipt --project ID
|
|
57
|
+
agentlinkops disavow list|history|propose|import|export|approve|reject|delete
|
|
58
|
+
agentlinkops index import FILE | export [--out FILE] | csv URLS.csv [--out FILE]
|
|
59
|
+
agentlinkops index check-csv URLS.csv --connection-id ID --request-id ID --out FILE
|
|
48
60
|
agentlinkops import FILE --target DOMAIN [--from SUPPLIER] [--map source=COL,target=COL]
|
|
49
61
|
[--exact-url] [--no-subdomains] [--generated-at ISO] [--json]
|
|
50
62
|
agentlinkops adopt CRM.sqlite [--scope exact|domain] [--write]
|
|
@@ -61,7 +73,7 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
|
|
|
61
73
|
agentlinkops citation run PANEL.json [--dir DIR] [--samples N] [--max-usd USD] [--json]
|
|
62
74
|
one fixed-n AI-citation epoch; evidence under
|
|
63
75
|
.agentlinkops/citations/; live engines need
|
|
64
|
-
|
|
76
|
+
engine-specific setup; the mock engine makes no paid calls
|
|
65
77
|
agentlinkops receive --body FILE --headers FILE signed notification; authenticated pull
|
|
66
78
|
agentlinkops connect --workspace ID --project-id ID [--origin URL] [--selection FILE]
|
|
67
79
|
agentlinkops tools [TOOLSET] [--json] [--refresh] cloud commands by toolset (offline snapshot;
|
|
@@ -74,7 +86,9 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
|
|
|
74
86
|
detected agent client (Claude Code, Codex, Cursor,
|
|
75
87
|
Gemini CLI, Hermes); stores no credential
|
|
76
88
|
agentlinkops agent status what is installed where
|
|
77
|
-
agentlinkops
|
|
89
|
+
agentlinkops agent remove [--scope project|user] [--apply] [--json]
|
|
90
|
+
agentlinkops agent recover [--scope project|user] [--apply] [--json]
|
|
91
|
+
agentlinkops sync [--push-only] [--pull-only] [--dry-run] [--include-wanted] [--lifecycle]
|
|
78
92
|
AGENTLINKOPS_TOKEN or AGENTLINKOPS_API_KEY
|
|
79
93
|
agentlinkops check [--filter TEXT] [--all] [--json] [--fail-on-unknown]
|
|
80
94
|
agentlinkops doctor verify ledger, verifier, cloud reachability, token
|
|
@@ -86,7 +100,12 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
|
|
|
86
100
|
agentlinkops status [--json]
|
|
87
101
|
agentlinkops diff [--json]
|
|
88
102
|
agentlinkops report [--out FILE] [--title T] [--brand B] [--as-of ISO]
|
|
89
|
-
[--include-notes] [--include-retired] [--digest-only]
|
|
103
|
+
[--include-notes] [--include-retired] [--digest-only] [--json]
|
|
104
|
+
HTML by default; --json writes the same frozen
|
|
105
|
+
dataset as JSON (the machine receipt)
|
|
106
|
+
agentlinkops locate (--entry ID [--at ISO] | --ref REFERENCE)
|
|
107
|
+
resolve one observation to its occurrences and
|
|
108
|
+
evidence reference, from the local mirror only
|
|
90
109
|
agentlinkops fmt
|
|
91
110
|
agentlinkops compact [--apply]
|
|
92
111
|
|
|
@@ -112,7 +131,7 @@ function reportProblems(problems, what, out) {
|
|
|
112
131
|
if (problems.length > 20) out(` … and ${problems.length - 20} more`);
|
|
113
132
|
}
|
|
114
133
|
|
|
115
|
-
export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), out = console.log, err = console.error } = {}) {
|
|
134
|
+
export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), out = console.log, err = console.error, env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
116
135
|
if (argv[0] === 'gsc-links') return await gscLinksMain(argv, { cwd, out });
|
|
117
136
|
// Citation watches keep their own files under .agentlinkops/citations/ and never
|
|
118
137
|
// touch the link ledger, so they dispatch before its requirement like context does.
|
|
@@ -126,8 +145,12 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
126
145
|
|
|
127
146
|
let syncLock = null;
|
|
128
147
|
try {
|
|
148
|
+
if (command === 'lifecycle') return await lifecycleMain(args,{cwd,env,fetchImpl,out});
|
|
149
|
+
if (command === 'admission') { const config=await loadConfig({cwd}); return await admissionMain(argv.slice(1),{client:createClient({...cloudConnection(config,env),fetchImpl}),out,err}); }
|
|
150
|
+
if (command === 'disavow') return await disavowMain(args,{cwd,env,fetchImpl,out});
|
|
151
|
+
if (command === 'index') return await indexMain(args,{cwd,env,fetchImpl,out});
|
|
129
152
|
if (command === 'setup') return await setupPlanMain(argv, { cwd, out });
|
|
130
|
-
if (command === 'tools' || command === 'describe' || command === 'call') return await commandsMain(argv, { cwd, out, err });
|
|
153
|
+
if (command === 'tools' || command === 'describe' || command === 'call') return await commandsMain(argv, { cwd, out, err, env, fetchImpl });
|
|
131
154
|
if (command === 'skill') return await skillMain(argv, { out });
|
|
132
155
|
if (command === 'agent') return await agentMain(argv, { cwd, out, err });
|
|
133
156
|
if (command === 'connect') {
|
|
@@ -369,8 +392,26 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
369
392
|
title: args.title === true ? 'Backlink report' : (args.title ?? 'Backlink report'),
|
|
370
393
|
includeNotes: args['include-notes'] === true,
|
|
371
394
|
includeRetired: args['include-retired'] === true,
|
|
395
|
+
indexObservations: await readIndexReceipts(config.paths.indexObservations),
|
|
372
396
|
});
|
|
373
397
|
if (args['digest-only']) { out(datasetDigest(dataset)); return 0; }
|
|
398
|
+
// `--json` selects the machine receipt: the SAME frozen dataset, not a second serializer,
|
|
399
|
+
// so an agent's JSON and a client's HTML cannot disagree about what was observed.
|
|
400
|
+
if (args.json) {
|
|
401
|
+
const json = JSON.stringify(dataset, null, 2);
|
|
402
|
+
if (args.out && args.out !== true) {
|
|
403
|
+
await mkdir(dirname(String(args.out)), { recursive: true });
|
|
404
|
+
await writeFile(String(args.out), json, 'utf8');
|
|
405
|
+
out(`${dataset.totals.entries} link(s), digest ${datasetDigest(dataset)}`);
|
|
406
|
+
out(`wrote ${args.out}`);
|
|
407
|
+
} else { out(json); }
|
|
408
|
+
// On the JSON stdout path the notices go to stderr: a consumer piping the machine
|
|
409
|
+
// receipt must receive exactly one parseable document, nothing after it.
|
|
410
|
+
const note = args.out && args.out !== true ? out : err;
|
|
411
|
+
if (!asOf) note('NOT reproducible: no --as-of, so the prepared date is today and tomorrow differs.');
|
|
412
|
+
if (dataset.include_notes) note('Private notes are IN this document.');
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
374
415
|
const html = renderReport(dataset, { brand: args.brand === true ? null : (args.brand ?? null) });
|
|
375
416
|
if (args.out && args.out !== true) {
|
|
376
417
|
await mkdir(dirname(String(args.out)), { recursive: true });
|
|
@@ -385,6 +426,55 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
385
426
|
return 0;
|
|
386
427
|
}
|
|
387
428
|
|
|
429
|
+
if (command === 'locate') {
|
|
430
|
+
// The resolution side of the evidence reference (DP-0004-T21): a reference printed in an
|
|
431
|
+
// export must resolve to the observation it names, from the repository alone, with no
|
|
432
|
+
// account and no network. Hosted references name the workspace's own rows and resolve
|
|
433
|
+
// through the API; sending a reader there from an offline command would be a silent
|
|
434
|
+
// dependency on live session state, so it is refused with the command that works.
|
|
435
|
+
const ref = args.ref === true ? null : (args.ref ?? null);
|
|
436
|
+
const entryId = args.entry === true ? null : (args.entry ?? null);
|
|
437
|
+
const at = args.at === true ? null : (args.at ?? null);
|
|
438
|
+
let row = null;
|
|
439
|
+
if (ref) {
|
|
440
|
+
const parsed = parseEvidenceReference(String(ref));
|
|
441
|
+
if (!parsed) { err(`not an AgentLinkOps evidence reference: ${ref}`); return 2; }
|
|
442
|
+
if (parsed.subject === 'index') {
|
|
443
|
+
if(parsed.storage==='hosted'){err(describeEvidenceReference(ref).resolves[0]);return 2;}
|
|
444
|
+
out(JSON.stringify(locateIndexReceipt(await readIndexReceipts(config.paths.indexObservations),parsed.receiptId),null,2));return 0;
|
|
445
|
+
}
|
|
446
|
+
if (parsed.storage === 'hosted') {
|
|
447
|
+
err('hosted references resolve through the workspace API, not the local mirror:');
|
|
448
|
+
err(` agentlinkops call locate_link --set observationId=${parsed.observationId}`);
|
|
449
|
+
return 2;
|
|
450
|
+
}
|
|
451
|
+
row = observations.rows.find(candidate => candidate.id === parsed.entryId
|
|
452
|
+
&& String(candidate.checked_at) === parsed.checkedAt
|
|
453
|
+
&& (candidate.source === 'cloud' ? 'cloud' : 'local') === parsed.origin) ?? null;
|
|
454
|
+
if (!row) { err(`no observation in this mirror matches ${ref}`); return 2; }
|
|
455
|
+
} else if (entryId) {
|
|
456
|
+
if (at && !Number.isFinite(Date.parse(String(at)))) { err(`--at is not a valid timestamp: ${at}`); return 2; }
|
|
457
|
+
const candidates = observations.rows.filter(candidate => candidate.id === String(entryId));
|
|
458
|
+
// `--at` is exact or it is an error: silently falling back to the latest observation
|
|
459
|
+
// would show a reader a different check while appearing to resolve the one they named.
|
|
460
|
+
row = at ? candidates.find(candidate => String(candidate.checked_at) === String(at)) ?? null
|
|
461
|
+
// Latest by checked_at, matching latestByEntry's rule, so `locate` and `report` never
|
|
462
|
+
// disagree about which observation a row's receipt describes.
|
|
463
|
+
: candidates.reduce((best, candidate) => (!best || String(candidate.checked_at) >= String(best.checked_at) ? candidate : best), null);
|
|
464
|
+
if (!row) { err(at ? `no observation of ${entryId} at ${at}` : `no observation of ${entryId} in this mirror`); return 2; }
|
|
465
|
+
} else { err('agentlinkops locate (--entry ID [--at ISO] | --ref REFERENCE)'); return 2; }
|
|
466
|
+
const entry = ledger.entries.find(candidate => candidate.id === row.id) ?? null;
|
|
467
|
+
const reference = mirrorEvidenceReference({ entryId: row.id, checkedAt: row.checked_at, origin: row.source === 'cloud' ? 'cloud' : 'local' });
|
|
468
|
+
out(JSON.stringify({
|
|
469
|
+
// The T05 locate view: positions, live-page handoff, evidence metadata, notes.
|
|
470
|
+
...locateOccurrences({ ...row, id: row.cloud_observation_id ?? null }),
|
|
471
|
+
ledger_entry: entry ? { id: entry.id, source: entry.source, target: entry.target, scope: entry.scope } : null,
|
|
472
|
+
evidence_reference: reference,
|
|
473
|
+
reference_resolution: describeEvidenceReference(reference),
|
|
474
|
+
}, null, 2));
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
388
478
|
if (command === 'fmt') {
|
|
389
479
|
reportProblems(ledger.problems, 'ledger line(s)', out);
|
|
390
480
|
if (ledger.problems.length) return 2;
|
|
@@ -438,6 +528,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
438
528
|
}
|
|
439
529
|
const state = await readState(config.paths.state);
|
|
440
530
|
if (args['pull-only'] && args['push-only']) throw new ConfigError('Choose --pull-only or --push-only.');
|
|
531
|
+
if(args.lifecycle&&args['push-only'])throw new ConfigError('--lifecycle requires a pull; omit --push-only.');
|
|
441
532
|
const selection = { includeWanted: args['include-wanted'] === true, ledgerIds: config.cloud?.ledgerIds ?? null };
|
|
442
533
|
if (args['dry-run']) { out(JSON.stringify({ network: false, rows: args['pull-only'] ? [] : syncPlan(ledger.entries, state, selection) }, null, 2)); return 0; }
|
|
443
534
|
const connection = cloudConnection(config);
|
|
@@ -467,6 +558,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
467
558
|
// expiry on one cannot move the other.
|
|
468
559
|
const targets = await pullTargetEvents(client, next);
|
|
469
560
|
const index = await watchIndex(client, next);
|
|
561
|
+
if(args.lifecycle)next.lifecycle=await pullLifecycleMirrors(client,next,{projectId:connection.projectId,index,ledgerIds:selection.ledgerIds??ledger.entries.map(entry=>entry.id)});
|
|
470
562
|
const { fresh, duplicates } = await persistPulledHistory(config, pulled.events, targets.events, index);
|
|
471
563
|
next.cursors = { ...(next.cursors ?? {}), events: pulled.cursor, target_events: targets.cursor };
|
|
472
564
|
next.gaps = [...(next.gaps ?? []), ...pulled.gaps, ...targets.gaps];
|
package/cli/mirror.js
CHANGED
|
File without changes
|
package/cli/mix.js
CHANGED
|
File without changes
|
package/cli/platforms.js
CHANGED
|
File without changes
|
|
File without changes
|
package/cli/receipts.js
CHANGED
|
File without changes
|
package/cli/report.js
CHANGED
|
@@ -12,10 +12,19 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Self-contained: no external stylesheet, font, script or image. That makes it openable offline
|
|
14
14
|
// forever, printable to PDF from any browser, and incapable of telling anyone it was opened.
|
|
15
|
+
//
|
|
16
|
+
// v2 makes the document a RECEIPT (DP-0004-T20): every row carries the observation behind it —
|
|
17
|
+
// when it was observed, the content hash of what was captured, who checked, and a stable
|
|
18
|
+
// evidence reference that resolves to the observation itself. One dataset feeds both exports:
|
|
19
|
+
// this HTML view for a human client, and `report --json` for an agent, so the two can never
|
|
20
|
+
// disagree about what was observed.
|
|
21
|
+
import {indexRowsForUrl} from './index-observations.js';
|
|
22
|
+
import {reconcileIndexObservations} from '../src/index-observation.js';
|
|
15
23
|
import { createHash } from 'node:crypto';
|
|
16
24
|
import { latestByEntry } from './mirror.js';
|
|
25
|
+
import { mirrorEvidenceReference } from '../src/evidence-reference.js';
|
|
17
26
|
|
|
18
|
-
export const REPORT_VERSION =
|
|
27
|
+
export const REPORT_VERSION = 2;
|
|
19
28
|
|
|
20
29
|
/** HTML-escape. Every value below is publisher or customer content and none of it is trusted. */
|
|
21
30
|
export const escape = value => String(value ?? '')
|
|
@@ -33,12 +42,14 @@ const STATE_LABEL = Object.freeze({
|
|
|
33
42
|
* Selection happens here and nowhere else, so a caller can hold this object, assert it, and know
|
|
34
43
|
* the rendered document is a pure function of it.
|
|
35
44
|
*/
|
|
36
|
-
export function freezeDataset(entries, observations, { asOf, title = 'Backlink report', includeNotes = false, includeRetired = false } = {}) {
|
|
45
|
+
export function freezeDataset(entries, observations, { asOf, title = 'Backlink report', includeNotes = false, includeRetired = false, indexObservations = [] } = {}) {
|
|
37
46
|
const latest = latestByEntry(observations);
|
|
38
47
|
const rows = entries
|
|
39
48
|
.filter(entry => includeRetired || entry.intent !== 'retired')
|
|
40
49
|
.map(entry => {
|
|
41
50
|
const row = latest.get(entry.id) ?? null;
|
|
51
|
+
const indexRows=indexRowsForUrl(indexObservations,entry.source);
|
|
52
|
+
const origin = row?.source === 'cloud' ? 'cloud' : (row?.source ?? null);
|
|
42
53
|
return {
|
|
43
54
|
id: entry.id, intent: entry.intent, source: entry.source, target: entry.target, scope: entry.scope,
|
|
44
55
|
expected_anchor: entry.expect?.anchor ?? null,
|
|
@@ -49,8 +60,34 @@ export function freezeDataset(entries, observations, { asOf, title = 'Backlink r
|
|
|
49
60
|
checked_by: row?.source ?? null,
|
|
50
61
|
anchor: row?.result?.occurrences?.[0]?.anchor ?? null,
|
|
51
62
|
rel: row?.result?.occurrences?.[0]?.rel ?? [],
|
|
63
|
+
// The receipt block: what was observed, when, by which check, and how to point at the
|
|
64
|
+
// observation itself. Null on a never-checked row — an absent receipt is a fact here,
|
|
65
|
+
// not a missing field.
|
|
66
|
+
evidence: row ? {
|
|
67
|
+
observed_at: row.checked_at ?? null,
|
|
68
|
+
// The content hash of the captured document, present whenever the check recorded one.
|
|
69
|
+
// This is the field anchoring (DP-0042-T07) will make independently verifiable; it is
|
|
70
|
+
// in the export now so a document made today can be verified the day anchoring lands.
|
|
71
|
+
content_hash: row.result?.evidence?.sha256 ?? null,
|
|
72
|
+
// The hosted snapshot key, when this observation was synced from the workspace.
|
|
73
|
+
// Local checks never store publisher bytes anywhere, so null there means exactly that.
|
|
74
|
+
snapshot_key: row.evidence_key ?? null,
|
|
75
|
+
// Who ran the check: a local CLI run or the hosted scheduler. Two origins at the same
|
|
76
|
+
// instant are two observations, and a receipt that blurred them would blur method.
|
|
77
|
+
origin,
|
|
78
|
+
checker_version: row.checker_version ?? row.result?.evidence?.checkerVersion ?? null,
|
|
79
|
+
// Named, not implied: a static observation is not a claim about what a reader sees.
|
|
80
|
+
method: row.result?.evidence?.rendered === true || row.result?.evidence?.method === 'browser_html'
|
|
81
|
+
? 'rendered' : row.result?.evidence?.rendered === false || row.result?.evidence?.method === 'http_html'
|
|
82
|
+
? 'static_html' : null,
|
|
83
|
+
// The hosted observation id when the mirror row carries one (cloud-synced rows).
|
|
84
|
+
hosted_observation_id: row.cloud_observation_id ?? null,
|
|
85
|
+
// The stable reference that resolves to this observation from the repository mirror.
|
|
86
|
+
locator: mirrorEvidenceReference({ entryId: entry.id, checkedAt: row.checked_at ?? undefined, origin: origin ?? undefined }),
|
|
87
|
+
} : null,
|
|
52
88
|
// Opt-in, because a note can hold commercial context — what a placement cost, what a
|
|
53
89
|
// publisher said — and a report is the document most likely to be forwarded.
|
|
90
|
+
...(indexRows.length?{index_observations:indexRows,index_reconciliation:reconcileIndexObservations(indexRows)}:{}),
|
|
54
91
|
note: includeNotes ? (entry.note ?? null) : null,
|
|
55
92
|
ref: includeNotes ? (entry.ref ?? null) : null,
|
|
56
93
|
};
|
|
@@ -94,6 +131,8 @@ td.url{word-break:break-all;max-width:340px}
|
|
|
94
131
|
.present{background:#eef7ee;border-color:#b6d7b6}
|
|
95
132
|
.absent{background:#fdeeee;border-color:#e2b6b6}
|
|
96
133
|
.unknown,.unchecked,.source_unavailable{background:#f6f6f6}
|
|
134
|
+
code.hash{font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
135
|
+
.ref{font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;word-break:break-all}
|
|
97
136
|
.note{color:#555;font-size:12px}
|
|
98
137
|
footer{margin-top:28px;padding-top:12px;border-top:1px solid #eee;color:#555;font-size:12px}
|
|
99
138
|
.caveat{margin:18px 0;padding:10px 14px;border-left:3px solid #ddd;color:#444;font-size:13px}
|
|
@@ -110,14 +149,36 @@ export function renderReport(dataset, { brand = null } = {}) {
|
|
|
110
149
|
const digest = createHash('sha256').update(JSON.stringify(dataset)).digest('hex');
|
|
111
150
|
const card = (label, value) => `<div class="card"><b>${escape(value)}</b><span>${escape(label)}</span></div>`;
|
|
112
151
|
const coverage = dataset.coverage;
|
|
152
|
+
// The receipt column. "hash not recorded" is stated rather than papered over: a local check
|
|
153
|
+
// that never completed a fetch has no hash, and a cloud-synced row may carry its snapshot key
|
|
154
|
+
// without a hash in an older event. Neither is a claim about the link.
|
|
155
|
+
const evidenceCell = row => {
|
|
156
|
+
if (!row.evidence) return '<span class="note">no observation</span>';
|
|
157
|
+
const hash = row.evidence.content_hash ? `<code class="hash">${escape(row.evidence.content_hash.slice(0, 12))}</code>` : '<span class="note">hash not recorded</span>';
|
|
158
|
+
const origin = row.evidence.origin === 'cloud' ? 'hosted check' : row.evidence.origin === 'local' ? 'local check' : 'unknown origin';
|
|
159
|
+
const checker = row.evidence.checker_version ? ` · checker ${escape(row.evidence.checker_version)}` : '';
|
|
160
|
+
const method=row.evidence.method==='rendered'?'browser-rendered HTML':row.evidence.method==='static_html'?'static HTML':'method not recorded';
|
|
161
|
+
return `${hash}<div class="note">${escape(origin)}${checker}</div><div class="note">${method}</div>`;
|
|
162
|
+
};
|
|
163
|
+
const hasIndex=dataset.rows.some(row=>row.index_observations?.length);
|
|
113
164
|
const rows = dataset.rows.map(row => `<tr>
|
|
114
165
|
<td class="url"><a href="${escape(row.source)}">${escape(row.source)}</a></td>
|
|
115
166
|
<td class="url">${escape(row.target)}</td>
|
|
116
167
|
<td><span class="state ${escape(row.state)}">${escape(STATE_LABEL[row.state] ?? row.state)}</span></td>
|
|
117
168
|
<td>${row.anchor ? escape(row.anchor) : '<span class="note">—</span>'}${(row.rel ?? []).length ? `<div class="note">rel: ${escape(row.rel.join(' '))}</div>` : ''}</td>
|
|
118
169
|
<td>${row.checked_at ? escape(row.checked_at.slice(0, 10)) : '<span class="note">never</span>'}</td>
|
|
170
|
+
<td>${evidenceCell(row)}</td>
|
|
171
|
+
${hasIndex?`<td>${(row.index_observations??[]).map(o=>`${escape(o.tier)} · ${escape(o.reason)}<div>${escape(o.checked_at)} · ${escape(o.backend)}</div><div class="ref">${escape(o.locator)}</div>`).join('<hr>')||'Not checked'}</td>`:''}
|
|
119
172
|
${dataset.include_notes ? `<td class="note">${escape(row.ref ?? '')}${row.note ? `<div>${escape(row.note)}</div>` : ''}</td>` : ''}
|
|
120
173
|
</tr>`).join('\n');
|
|
174
|
+
// Every row's full reference, printable: the table above stays readable while the document a
|
|
175
|
+
// client forwards still carries the complete handle for each observation behind it.
|
|
176
|
+
const receiptRows = dataset.rows.filter(row => row.evidence?.locator).map(row => `<tr>
|
|
177
|
+
<td>${escape(row.id)}</td>
|
|
178
|
+
<td>${escape(row.evidence.observed_at ?? 'unknown')}</td>
|
|
179
|
+
<td>${row.evidence.content_hash ? `<code class="hash">${escape(row.evidence.content_hash)}</code>` : '<span class="note">not recorded</span>'}</td>
|
|
180
|
+
<td class="ref">${escape(row.evidence.locator)}</td>
|
|
181
|
+
</tr>`).join('\n');
|
|
121
182
|
|
|
122
183
|
return `<!doctype html>
|
|
123
184
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -137,17 +198,29 @@ last observed, between ${escape(coverage.observed_from ?? 'n/a')} and ${escape(c
|
|
|
137
198
|
The date at the top is when this document was prepared and does not make any observation newer
|
|
138
199
|
than it is. ${coverage.never_checked ? `${coverage.never_checked} of ${dataset.totals.entries} links have never been checked and are listed as such.` : ''}</p>
|
|
139
200
|
<p class="caveat"><strong>What a result means.</strong> “Present” means the link was in
|
|
140
|
-
the HTML
|
|
201
|
+
the observed HTML. “Could not check” is not the same as
|
|
141
202
|
“not found”: it means the page could not be read, and no conclusion was drawn.
|
|
142
|
-
|
|
203
|
+
Each receipt identifies the check method when it was recorded. A snapshot alone does not prove the link was visible on screen.</p>
|
|
143
204
|
<table><thead><tr>
|
|
144
|
-
<th>Source page</th><th>Target</th><th>Result</th><th>Anchor</th><th>Observed</th>${dataset.include_notes ? '<th>Notes</th>' : ''}
|
|
205
|
+
<th>Source page</th><th>Target</th><th>Result</th><th>Anchor</th><th>Observed</th><th>Evidence</th>${hasIndex?'<th>Index evidence</th>':''}${dataset.include_notes ? '<th>Notes</th>' : ''}
|
|
145
206
|
</tr></thead><tbody>
|
|
146
207
|
${rows}
|
|
147
208
|
</tbody></table>
|
|
209
|
+
${receiptRows ? `<h2>Evidence references</h2>
|
|
210
|
+
<p class="caveat"><strong>What a reference is.</strong> Each row above was concluded from one
|
|
211
|
+
saved observation. Its reference names that observation — which ledger entry, when it was
|
|
212
|
+
checked, and whether the check ran locally or on the hosted service — and resolves to the
|
|
213
|
+
recorded result from the repository that produced this document, with no account required. The
|
|
214
|
+
content hash, where recorded, is the hash of the document that was captured at that moment;
|
|
215
|
+
two captures of a changed page produce different hashes, which is what makes "the same
|
|
216
|
+
evidence" checkable rather than claimed.</p>
|
|
217
|
+
<table><thead><tr><th>Link</th><th>Observed at</th><th>Content hash</th><th>Reference</th></tr></thead><tbody>
|
|
218
|
+
${receiptRows}
|
|
219
|
+
</tbody></table>` : ''}
|
|
148
220
|
<footer>
|
|
149
221
|
AgentLinkOps report v${dataset.v}. Dataset digest <code>${escape(digest)}</code> — the same
|
|
150
|
-
ledger and observations always produce this digest.
|
|
222
|
+
ledger and observations always produce this digest. The same dataset exports as JSON
|
|
223
|
+
(<code>agentlinkops report --json</code>) and carries the same digest.
|
|
151
224
|
${dataset.include_notes ? 'Private notes are included in this document.' : 'Private notes and references are excluded.'}
|
|
152
225
|
</footer>
|
|
153
226
|
</body></html>
|
package/cli/setup-plan.js
CHANGED
|
File without changes
|
package/cli/skill.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import { join, dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
// steps); a machine reader of `tools --json` or of stderr error objects never sees it.
|
|
12
12
|
export const REFERENCE_URL = 'https://agentlinkops.com/SKILL.md';
|
|
13
13
|
export const NUDGE = `Run \`agentlinkops skill\` and read it in full before the first AgentLinkOps call in a session (also at ${REFERENCE_URL}).`;
|
|
14
|
-
const USAGE = 'skill [--url]';
|
|
14
|
+
const USAGE = 'skill [--url | --list [--json] | NAME]';
|
|
15
15
|
|
|
16
16
|
export async function readReference({ from = fileURLToPath(import.meta.url), packDir = null } = {}) {
|
|
17
17
|
// The pack beside the CLI (the published package), then this repository's generated copy,
|
|
@@ -29,10 +29,47 @@ export async function readReference({ from = fileURLToPath(import.meta.url), pac
|
|
|
29
29
|
throw new Error(`No agent reference found beside this CLI; read ${REFERENCE_URL}.`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export async function readSkillSet({ from = fileURLToPath(import.meta.url), packDir = null } = {}) {
|
|
33
|
+
const base = dirname(from);
|
|
34
|
+
const packs = packDir ? [packDir] : [join(base, '..', 'skills'), join(base, '..', 'toolkit', 'plugin', 'agentlinkops', 'skills'), join(base, '..', 'dist', 'plugin', 'agentlinkops', 'skills')];
|
|
35
|
+
for (const pack of packs) {
|
|
36
|
+
const entries = await readdir(pack, { withFileTypes: true }).catch(error => { if (error.code === 'ENOENT') return []; throw error; });
|
|
37
|
+
const skills = [];
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!entry.isDirectory() || !/^agentlinkops-[a-z0-9-]+$/.test(entry.name)) continue;
|
|
40
|
+
const text = await readFile(join(pack, entry.name, 'SKILL.md'), 'utf8').catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
41
|
+
if (!text) continue;
|
|
42
|
+
const frontmatter = text.match(/^---\n([\s\S]*?)\n---/u)?.[1] ?? '';
|
|
43
|
+
const description = frontmatter.match(/^description:\s*(.+)$/mu)?.[1] ?? '';
|
|
44
|
+
skills.push({ name: entry.name, description, text, source: join(pack, entry.name, 'SKILL.md') });
|
|
45
|
+
}
|
|
46
|
+
if (!skills.length) continue;
|
|
47
|
+
let version = 'unversioned';
|
|
48
|
+
for (const file of [join(pack, '..', '.claude-plugin', 'plugin.json'), join(pack, '..', 'package.json')]) {
|
|
49
|
+
const manifest = await readFile(file, 'utf8').catch(() => null);
|
|
50
|
+
if (manifest) { version = JSON.parse(manifest).version ?? version; break; }
|
|
51
|
+
}
|
|
52
|
+
return { version, skills: skills.sort((a, b) => a.name.localeCompare(b.name)) };
|
|
53
|
+
}
|
|
54
|
+
throw new Error('No shipped skill set found beside this CLI');
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
export async function skillMain(argv, { out = console.log, from, packDir = null } = {}) {
|
|
33
58
|
const rest = argv.slice(1);
|
|
34
|
-
if (rest.
|
|
35
|
-
if (rest.
|
|
59
|
+
if (rest.length === 1 && rest[0] === '--url') { out(REFERENCE_URL); return 0; }
|
|
60
|
+
if (rest[0] === '--list' && rest.every(flag => ['--list', '--json'].includes(flag))) {
|
|
61
|
+
const { version, skills } = await readSkillSet({ from, packDir });
|
|
62
|
+
const listing = { version, skills: skills.map(({ name, description }) => ({ name, description })) };
|
|
63
|
+
out(rest.includes('--json') ? JSON.stringify(listing) : `AgentLinkOps skills ${version}\n` + listing.skills.map(skill => `${skill.name} — ${skill.description}`).join('\n'));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
if (rest.length === 1 && /^agentlinkops-[a-z0-9-]+$/.test(rest[0])) {
|
|
67
|
+
const { skills } = await readSkillSet({ from, packDir });
|
|
68
|
+
const selected = skills.find(skill => skill.name === rest[0]);
|
|
69
|
+
if (!selected) { out('Unknown shipped skill; run agentlinkops skill --list'); return 2; }
|
|
70
|
+
out(`<!-- Installed skill source: ${selected.source} -->\n${selected.text.trimEnd()}`); return 0;
|
|
71
|
+
}
|
|
72
|
+
if (rest.length) { out(USAGE); return 2; }
|
|
36
73
|
const { text } = await readReference({ from, packDir });
|
|
37
74
|
out(text.replace(/^<!--[^\n]*-->\n/u, '').trimEnd());
|
|
38
75
|
return 0;
|
package/cli/state.js
CHANGED
|
File without changes
|
package/cli/status.js
CHANGED
|
File without changes
|
package/cli/sync.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {lifecycleOutput} from '../shared/lifecycle-contract.js';
|
|
1
2
|
import {observationFromSnapshot,eventSnapshot} from '../shared/observation-event.js';
|
|
2
3
|
// `agentlinkops sync`: expectations up, events and evidence references down.
|
|
3
4
|
//
|
|
@@ -151,7 +152,11 @@ export function cloudObservationRows(events, watchToLedger) {
|
|
|
151
152
|
if (!ledgerId) continue;
|
|
152
153
|
const observation = event.data?.after;
|
|
153
154
|
if (!observation?.checked_at) continue;
|
|
154
|
-
rows.push({...observationRow(ledgerId, observationFromSnapshot(observation), { source: 'cloud', evidenceKey: event.data?.evidence_key ?? null }),
|
|
155
|
+
rows.push({...observationRow(ledgerId, observationFromSnapshot(observation), { source: 'cloud', evidenceKey: event.data?.evidence_key ?? null }),
|
|
156
|
+
// The hosted observation's own id, carried so a receipt export can name the workspace row
|
|
157
|
+
// (agentlinkops:evidence/1/link/hosted/<id>) alongside the mirror reference that resolves
|
|
158
|
+
// offline. Absent on snapshot-derived rows, which reconstruct state without observation ids.
|
|
159
|
+
cloud_observation_id: event.data?.observation_id ?? null, projection_version: 1});
|
|
155
160
|
}
|
|
156
161
|
return rows;
|
|
157
162
|
}
|
|
@@ -177,3 +182,20 @@ export async function watchIndex(client, state) {
|
|
|
177
182
|
}
|
|
178
183
|
throw new CloudError('WATCH_INDEX_LIMIT', 502);
|
|
179
184
|
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
// Optional read-only deal mirror. One failure leaves the previous mirror untouched.
|
|
188
|
+
// The caller saves it only after every selected watch has been read and scoped.
|
|
189
|
+
export async function pullLifecycleMirrors(client,state,{projectId,index=null,ledgerIds=null,now=()=>new Date().toISOString()}={}){
|
|
190
|
+
const candidates=index?[...index].map(([watchId,ledgerId])=>[ledgerId,watchId]):Object.entries(state.watches??{});
|
|
191
|
+
const selected=ledgerIds===null?null:new Set(ledgerIds),mapping=candidates.filter(([id])=>!selected||selected.has(id));
|
|
192
|
+
if(new Set(mapping.map(([id])=>id)).size!==mapping.length)throw new CloudError('LIFECYCLE_MAPPING_CONFLICT',0);
|
|
193
|
+
if(mapping.length>1000)throw new CloudError('LIFECYCLE_MIRROR_LIMIT',0);
|
|
194
|
+
const rows={};
|
|
195
|
+
for(const [ledgerId,watchId] of mapping){
|
|
196
|
+
const value=lifecycleOutput.parse(await client.callCommand('get_link_lifecycle',{projectId,watchId}));
|
|
197
|
+
if(value.projectId!==projectId||value.watchId!==watchId)throw new CloudError('LIFECYCLE_MIRROR_SCOPE',0);
|
|
198
|
+
rows[ledgerId]={...value,observedAt:now()};
|
|
199
|
+
}
|
|
200
|
+
return {projectId,rows};
|
|
201
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlinkops",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "A backlink and AI citation ledger that lives in your repository: local link and citation checks with dated evidence, supplier-export imports and optional sync to the hosted AgentLinkOps service.",
|
|
5
5
|
"author": "AgentLinkOps (https://agentlinkops.com)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@modelcontextprotocol/server": "2.0.0",
|
|
60
60
|
"parse5": "8.0.1",
|
|
61
61
|
"turndown": "^7.2.4",
|
|
62
|
+
"undici": "7.29.0",
|
|
62
63
|
"zod": "4.6.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
@@ -67,4 +68,4 @@
|
|
|
67
68
|
"optionalDependencies": {
|
|
68
69
|
"playwright-core": "^1.63.0"
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Citation panels
|
|
2
|
+
|
|
3
|
+
A panel records the questions, target sites, engines and sample budget for a citation check. Start with questions your audience has actually asked. Keep each question's source and collection date in your own research notes. Search Console queries, support conversations and public discussion threads can help when you already have access; the panel builder does not fetch or validate them.
|
|
4
|
+
|
|
5
|
+
Create an editable draft without calling a provider:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
agentlinkops citation panel --domain example.com --brand Example --topic "backlink monitoring" --out panel.json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This command defaults to the mock engine. Its suggested questions are writing prompts, not measured search demand. Replace them with the questions you chose. Repeat `--prompt "Your question"` to supply your own questions directly. The command refuses to overwrite an existing output file.
|
|
12
|
+
|
|
13
|
+
Use `--competitors competitors.json` to add domains to check beside your own. The file is an array of objects with `domain`, `brand` and optional `aliases` fields. Each target gets separate results for each question. A target missing from one answer is only missing from that sample; it does not establish absence from the engine.
|
|
14
|
+
|
|
15
|
+
Choose a live engine explicitly after checking access and budget. For an existing browser setup, use `--engine chatgpt:web-own-browser` or `--engine grok:web-own-browser`. `--engine google-aio` requires your configured supplier access. This workflow does not create accounts or buy services.
|
|
16
|
+
|
|
17
|
+
Set `--locale en-GB --country GB` for a British locale draft. The mock engine supports locale fixtures. The Google AIO supplier path supports US and GB location requests; other engine paths refuse non-default locale requests in this version. Legacy default browser observations do not prove US proxy egress. Reports disclose the requested context and its basis; they do not treat it as verified user geography.
|
|
18
|
+
|
|
19
|
+
After reviewing the draft, run the [local CLI workflow](cli.md) and inspect the retained evidence:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
agentlinkops citation run panel.json --max-usd 1
|
|
23
|
+
agentlinkops citation report panel.json --last 2 --out report.html
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Keep brand mentions separate from links. Retained answer text can mention a brand and cite it in the same observation. Unknown or failed answers remain unknown. Reports keep engine, question, target and non-default locale identities separate; they do not produce an engine-wide share-of-voice score.
|
package/references/cli.md
CHANGED
|
File without changes
|
package/references/disavow.md
CHANGED
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# From research to a checked shortlist
|
|
2
|
+
|
|
3
|
+
Keep research notes beside the user's project. The graph release, source retrieval date and qualification explain why a page is on the shortlist. They do not establish a live backlink.
|
|
4
|
+
|
|
5
|
+
Use `agentlinkops skill --list` to see the installed set and `agentlinkops skill agentlinkops-discovery` to read the research workflow. `agentlinkops skill` still prints the connection and evidence reference.
|
|
6
|
+
|
|
7
|
+
Create a CSV with exact source and target pages:
|
|
8
|
+
|
|
9
|
+
```csv
|
|
10
|
+
source_url,target_url
|
|
11
|
+
https://publisher.example.org/resources,https://example.com/guide
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Preview normalization before writing:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
agentlinkops import shortlist.csv --target example.com --from csv --map source=source_url,target=target_url
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The preview is not a live check or a watch. Inspect accepted and rejected rows. For a selected opportunity that has not been earned, initialize a local ledger when needed and add it as wanted:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
agentlinkops init
|
|
24
|
+
agentlinkops add --source https://publisher.example.org/resources --target https://example.com/guide --intent wanted --note "Research candidate; link not yet earned"
|
|
25
|
+
agentlinkops check --all --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The local check includes wanted entries; `--all` rechecks the selected rows even if their cadence is not yet due. Keep observed absence separate from unknown access failures. Once the user has an earned placement, record the exact source/target pair as expected and retain the observation date and evidence.
|
|
29
|
+
|
|
30
|
+
For an already connected workspace, `agentlinkops tools monitoring` and `agentlinkops describe monitor_link` expose the current hosted contract. Inspect project IDs and usage first. Enroll selected source/target pairs only when recurring monitoring is within the user's request; record the returned watch IDs. A successful import or local check alone does not create a recurring hosted job.
|
|
31
|
+
|
|
32
|
+
The [campaign skill](../skills/agentlinkops-campaigns/SKILL.md) handles subsequent campaign judgment. The [CRM reference](cli.md) covers optional records when the user needs contacts and activities beside the ledger. Outreach stays in the user's chosen tool and requires their instruction.
|