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.
Files changed (201) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.mcp.json +0 -0
  4. package/CHANGELOG.md +17 -0
  5. package/LICENSE +0 -0
  6. package/NOTICE +0 -0
  7. package/README.md +0 -0
  8. package/bin/agentlinkops.mjs +0 -0
  9. package/bin/linktrail.mjs +0 -0
  10. package/cli/adapters/csv.js +0 -0
  11. package/cli/adapters/sqlite-crm.js +0 -0
  12. package/cli/adapters/suppliers.js +0 -0
  13. package/cli/admission.js +130 -0
  14. package/cli/agent-setup.js +1014 -101
  15. package/cli/agentlinkops.mjs +0 -0
  16. package/cli/args.js +0 -0
  17. package/cli/catalog.js +12837 -4694
  18. package/cli/check.js +0 -0
  19. package/cli/citation.js +181 -32
  20. package/cli/client.js +1 -1
  21. package/cli/commands.js +3 -3
  22. package/cli/config.js +2 -1
  23. package/cli/connection.js +0 -0
  24. package/cli/context-mcp.js +0 -0
  25. package/cli/context.js +0 -0
  26. package/cli/delivery.js +0 -0
  27. package/cli/disavow.js +54 -0
  28. package/cli/doctor.js +7 -8
  29. package/cli/env.js +0 -0
  30. package/cli/fleet.js +0 -0
  31. package/cli/gsc-browser.js +0 -0
  32. package/cli/import.js +0 -0
  33. package/cli/index-observations.js +89 -0
  34. package/cli/ledger.js +7 -1
  35. package/cli/lifecycle.js +40 -0
  36. package/cli/linktrail.mjs +0 -0
  37. package/cli/local-result.js +0 -0
  38. package/cli/main.js +99 -7
  39. package/cli/mirror.js +0 -0
  40. package/cli/mix.js +0 -0
  41. package/cli/platforms.js +0 -0
  42. package/cli/receipt-performance.js +0 -0
  43. package/cli/receipts.js +0 -0
  44. package/cli/report.js +79 -6
  45. package/cli/setup-plan.js +0 -0
  46. package/cli/skill.js +41 -4
  47. package/cli/state.js +0 -0
  48. package/cli/status.js +0 -0
  49. package/cli/sync.js +23 -1
  50. package/package.json +3 -2
  51. package/references/citation-panels.md +26 -0
  52. package/references/cli.md +0 -0
  53. package/references/disavow.md +0 -0
  54. package/references/discovery-handoff.md +32 -0
  55. package/references/recipes/catalog.json +148 -0
  56. package/references/recipes/command-notes.md +9 -0
  57. package/references/recipes/placement-reconciliation.md +41 -0
  58. package/references/recipes/qualified-campaign-handoff.md +56 -0
  59. package/references/recipes/site-context-brief.md +25 -0
  60. package/references/recipes/sourced-linkable-asset.md +25 -0
  61. package/references/reports.md +0 -0
  62. package/scripts/agentlinkops.py +0 -0
  63. package/scripts/disavow.py +0 -0
  64. package/scripts/linktrail.py +0 -0
  65. package/scripts/migrations/001_crm.sql +0 -0
  66. package/scripts/migrations/002_cloud_events.sql +0 -0
  67. package/scripts/migrations/003_disavow.sql +0 -0
  68. package/scripts/migrations/004_placement_costs.sql +0 -0
  69. package/scripts/report.py +0 -0
  70. package/shared/admission-contract.js +16 -0
  71. package/shared/admission-report-contract.js +8 -0
  72. package/shared/agent-clients.js +0 -0
  73. package/shared/citation-evidence-contract.js +27 -0
  74. package/shared/disavow-contract.js +18 -0
  75. package/shared/index-observation-contract.js +14 -0
  76. package/shared/lifecycle-contract.js +22 -0
  77. package/shared/lifecycle-money.js +24 -0
  78. package/shared/observation-event.js +12 -1
  79. package/shared/opportunity-record.js +259 -0
  80. package/shared/response-contract.js +2 -0
  81. package/shared/search-connection-contract.js +0 -0
  82. package/skills/agentlinkops-assets/SKILL.md +2 -0
  83. package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
  84. package/skills/agentlinkops-campaigns/SKILL.md +5 -1
  85. package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
  86. package/skills/agentlinkops-connect/SKILL.md +8 -0
  87. package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
  88. package/skills/agentlinkops-connect/references/agentlinkops.md +9 -5
  89. package/skills/agentlinkops-crm/SKILL.md +2 -0
  90. package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
  91. package/skills/agentlinkops-discovery/SKILL.md +17 -0
  92. package/skills/content-defingerprinting/SKILL.md +0 -0
  93. package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
  94. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
  95. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
  96. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
  97. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
  98. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
  99. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
  100. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
  101. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
  102. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
  103. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
  104. package/skills/internal-linking-optimizer/SKILL.md +0 -0
  105. package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
  106. package/skills/pre-publish-review/SKILL.md +0 -0
  107. package/skills/source-cited-content-builder/SKILL.md +0 -0
  108. package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
  109. package/skills/source-cited-humanizer/SKILL.md +0 -0
  110. package/src/citations/adapter.js +102 -21
  111. package/src/citations/alert.js +38 -13
  112. package/src/citations/browser/engine.js +145 -50
  113. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
  114. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
  115. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
  116. package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
  117. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
  118. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
  119. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
  120. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
  121. package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
  122. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
  123. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
  124. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
  125. package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
  126. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
  127. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
  128. package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
  129. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
  130. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
  131. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
  132. package/src/citations/browser/gen/core/providers/index.js +0 -0
  133. package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
  134. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
  135. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
  136. package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
  137. package/src/citations/browser/gen/lib/browser/display.js +0 -0
  138. package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
  139. package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
  140. package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
  141. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
  142. package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
  143. package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
  144. package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
  145. package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
  146. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
  147. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
  148. package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
  149. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
  150. package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
  151. package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
  152. package/src/citations/browser/gen/utils/agent-constants.js +0 -0
  153. package/src/citations/browser/providers.js +3 -1
  154. package/src/citations/browser/shims/errors.js +0 -0
  155. package/src/citations/browser/shims/types.js +0 -0
  156. package/src/citations/browser/shims/utils.js +1 -0
  157. package/src/citations/contract.js +55 -9
  158. package/src/citations/inventory.js +106 -0
  159. package/src/citations/login.js +122 -0
  160. package/src/citations/match.js +12 -0
  161. package/src/citations/panel-builder.js +44 -39
  162. package/src/citations/report.js +88 -21
  163. package/src/citations/runner.js +59 -31
  164. package/src/citations/stats.js +133 -61
  165. package/src/citations/sync.js +131 -0
  166. package/src/citations/watch.js +57 -77
  167. package/src/competitors/contract.js +0 -0
  168. package/src/competitors/domain-mix.js +0 -0
  169. package/src/context/ga4.js +0 -0
  170. package/src/context/gsc.js +0 -0
  171. package/src/context/site-profile.js +0 -0
  172. package/src/context/util.js +0 -0
  173. package/src/context-tool-registry.js +0 -0
  174. package/src/corpus/acquire.js +47 -8
  175. package/src/corpus/seeds.js +132 -19
  176. package/src/discovery/contract.js +0 -0
  177. package/src/discovery/import.js +0 -0
  178. package/src/evidence-reference.js +112 -0
  179. package/src/index-observation.js +161 -0
  180. package/src/locator.js +112 -0
  181. package/src/tool-views.js +6 -2
  182. package/src/verifier/fetch.js +42 -6
  183. package/src/verifier/index.js +0 -0
  184. package/src/verifier/observation-kind.js +0 -0
  185. package/src/verifier/parser.js +0 -0
  186. package/src/verifier/readiness.js +0 -0
  187. package/src/verifier/render-evidence.js +0 -0
  188. package/src/verifier/robots.js +0 -0
  189. package/src/verifier/transition.js +0 -0
  190. package/src/verifier/url.js +0 -0
  191. package/templates/SITE.template.md +0 -0
  192. package/templates/campaigns.v1.json +0 -0
  193. package/src/citations/browser/gen/core/providers/_shared/google.js +0 -4
  194. package/src/citations/browser/gen/core/providers/types.js +0 -0
  195. package/src/citations/browser/gen/lib/browser/playwrightCompat.js +0 -321
  196. package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +0 -251
  197. package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
  198. package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +0 -54
  199. package/src/citations/browser/gen/lib/input/editor/findSendButton.js +0 -21
  200. package/src/citations/browser/gen/lib/input/response/getText.js +0 -10
  201. package/src/citations/fixtures/dataforseo-aio.json +0 -82
@@ -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
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] print the agent reference; read it in full once per session
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
- PERPLEXITY_API_KEY, the mock engine costs nothing
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 sync [--push-only] [--pull-only] [--dry-run] [--include-wanted]
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 = 1;
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 ? ` &middot; 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">&mdash;</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> &ldquo;Present&rdquo; means the link was in
140
- the HTML the publisher served. &ldquo;Could not check&rdquo; is not the same as
201
+ the observed HTML. &ldquo;Could not check&rdquo; is not the same as
141
202
  &ldquo;not found&rdquo;: it means the page could not be read, and no conclusion was drawn.
142
- JavaScript execution and visual visibility were not checked.</p>
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> &mdash; 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.some(a => !['--url'].includes(a))) { out(USAGE); return 2; }
35
- if (rest.includes('--url')) { out(REFERENCE_URL); return 0; }
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 }), projection_version: 1});
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.1",
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
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.