agentlinkops 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +24 -0
  3. package/CHANGELOG.md +52 -0
  4. package/LICENSE +190 -0
  5. package/NOTICE +17 -0
  6. package/README.md +202 -0
  7. package/bin/agentlinkops.mjs +4 -0
  8. package/bin/linktrail.mjs +6 -0
  9. package/cli/adapters/csv.js +68 -0
  10. package/cli/adapters/sqlite-crm.js +79 -0
  11. package/cli/adapters/suppliers.js +180 -0
  12. package/cli/agent-setup.js +203 -0
  13. package/cli/agentlinkops.mjs +4 -0
  14. package/cli/args.js +18 -0
  15. package/cli/catalog.js +7483 -0
  16. package/cli/check.js +119 -0
  17. package/cli/citation.js +109 -0
  18. package/cli/client.js +60 -0
  19. package/cli/commands.js +183 -0
  20. package/cli/config.js +104 -0
  21. package/cli/connection.js +90 -0
  22. package/cli/context-mcp.js +193 -0
  23. package/cli/context.js +340 -0
  24. package/cli/delivery.js +67 -0
  25. package/cli/doctor.js +283 -0
  26. package/cli/env.js +66 -0
  27. package/cli/fleet.js +82 -0
  28. package/cli/gsc-browser.js +239 -0
  29. package/cli/import.js +108 -0
  30. package/cli/ledger.js +232 -0
  31. package/cli/linktrail.mjs +6 -0
  32. package/cli/local-result.js +161 -0
  33. package/cli/main.js +523 -0
  34. package/cli/mirror.js +126 -0
  35. package/cli/mix.js +123 -0
  36. package/cli/platforms.js +161 -0
  37. package/cli/receipt-performance.js +64 -0
  38. package/cli/receipts.js +122 -0
  39. package/cli/report.js +158 -0
  40. package/cli/setup-plan.js +211 -0
  41. package/cli/skill.js +39 -0
  42. package/cli/state.js +106 -0
  43. package/cli/status.js +51 -0
  44. package/cli/sync.js +179 -0
  45. package/package.json +59 -0
  46. package/references/cli.md +198 -0
  47. package/references/disavow.md +73 -0
  48. package/references/reports.md +53 -0
  49. package/scripts/agentlinkops.py +651 -0
  50. package/scripts/disavow.py +207 -0
  51. package/scripts/linktrail.py +10 -0
  52. package/scripts/migrations/001_crm.sql +45 -0
  53. package/scripts/migrations/002_cloud_events.sql +13 -0
  54. package/scripts/migrations/003_disavow.sql +20 -0
  55. package/scripts/migrations/004_placement_costs.sql +2 -0
  56. package/scripts/report.py +113 -0
  57. package/shared/agent-clients.js +49 -0
  58. package/shared/observation-event.js +43 -0
  59. package/shared/response-contract.js +59 -0
  60. package/shared/search-connection-contract.js +18 -0
  61. package/skills/agentlinkops-assets/SKILL.md +13 -0
  62. package/skills/agentlinkops-assets/agents/openai.yaml +5 -0
  63. package/skills/agentlinkops-campaigns/SKILL.md +17 -0
  64. package/skills/agentlinkops-campaigns/agents/openai.yaml +12 -0
  65. package/skills/agentlinkops-connect/SKILL.md +33 -0
  66. package/skills/agentlinkops-connect/agents/openai.yaml +12 -0
  67. package/skills/agentlinkops-connect/references/agentlinkops.md +266 -0
  68. package/skills/agentlinkops-crm/SKILL.md +27 -0
  69. package/skills/agentlinkops-crm/agents/openai.yaml +12 -0
  70. package/skills/content-defingerprinting/SKILL.md +29 -0
  71. package/skills/content-defingerprinting/scripts/watermarks/README.md +102 -0
  72. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +26 -0
  73. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +52 -0
  74. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +21 -0
  75. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +96 -0
  76. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +432 -0
  77. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +86 -0
  78. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +460 -0
  79. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +651 -0
  80. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +27 -0
  81. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +212 -0
  82. package/skills/internal-linking-optimizer/SKILL.md +56 -0
  83. package/skills/internal-linking-optimizer/references/source-prompt.md +76 -0
  84. package/skills/pre-publish-review/SKILL.md +179 -0
  85. package/skills/source-cited-content-builder/SKILL.md +56 -0
  86. package/skills/source-cited-content-builder/references/source-prompt.md +19 -0
  87. package/skills/source-cited-humanizer/SKILL.md +20 -0
  88. package/src/citations/adapter.js +141 -0
  89. package/src/citations/contract.js +171 -0
  90. package/src/citations/match.js +120 -0
  91. package/src/citations/runner.js +151 -0
  92. package/src/citations/stats.js +74 -0
  93. package/src/competitors/contract.js +157 -0
  94. package/src/competitors/domain-mix.js +199 -0
  95. package/src/context/ga4.js +652 -0
  96. package/src/context/gsc.js +925 -0
  97. package/src/context/site-profile.js +445 -0
  98. package/src/context/util.js +71 -0
  99. package/src/context-tool-registry.js +27 -0
  100. package/src/corpus/acquire.js +135 -0
  101. package/src/corpus/seeds.js +182 -0
  102. package/src/discovery/contract.js +237 -0
  103. package/src/discovery/import.js +156 -0
  104. package/src/tool-views.js +117 -0
  105. package/src/verifier/fetch.js +303 -0
  106. package/src/verifier/index.js +311 -0
  107. package/src/verifier/observation-kind.js +91 -0
  108. package/src/verifier/parser.js +241 -0
  109. package/src/verifier/readiness.js +90 -0
  110. package/src/verifier/render-evidence.js +57 -0
  111. package/src/verifier/robots.js +85 -0
  112. package/src/verifier/transition.js +129 -0
  113. package/src/verifier/url.js +56 -0
  114. package/templates/SITE.template.md +59 -0
  115. package/templates/campaigns.v1.json +65 -0
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "agentlinkops",
3
+ "version": "0.5.0",
4
+ "description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.",
5
+ "author": {
6
+ "name": "AgentLinkOps"
7
+ },
8
+ "skills": "./skills/",
9
+ "mcpServers": "./.mcp.json"
10
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "agentlinkops",
3
+ "version": "0.5.0",
4
+ "description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.",
5
+ "author": {
6
+ "name": "AgentLinkOps"
7
+ },
8
+ "skills": "./skills/",
9
+ "interface": {
10
+ "displayName": "AgentLinkOps",
11
+ "shortDescription": "Run backlink campaigns with your own agent.",
12
+ "longDescription": "Plan campaigns and preserve contacts, outreach references and placements in a local SQLite CRM. Manage property-scoped disavow lists and generate branded HTML reports offline. Optional account-connected event sync preserves local notes and costs.",
13
+ "developerName": "AgentLinkOps",
14
+ "category": "Productivity",
15
+ "capabilities": [
16
+ "Write"
17
+ ],
18
+ "defaultPrompt": [
19
+ "Plan a backlink campaign for my product.",
20
+ "Set up my local link and outreach CRM.",
21
+ "Review changes to my monitored placements."
22
+ ]
23
+ }
24
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `agentlinkops` package are recorded here. The version follows the
4
+ plugin manifests in `.claude-plugin/` and `.codex-plugin/`.
5
+
6
+ ## 0.5.0 (unreleased; DP-0036 agent surface)
7
+
8
+ - `agentlinkops tools [TOOLSET]`, `agentlinkops describe NAME` and `agentlinkops call NAME`:
9
+ the three discovery verbs, answered offline from the bundled catalog snapshot (`--refresh`
10
+ reads the live one). The index prints each toolset's summary; `call` adds `--set`,
11
+ `--dry-run` and `-y`, and retired command names resolve to their canonical command.
12
+ - `agentlinkops agent setup` and `agentlinkops agent status`: install the skill pack and the
13
+ right MCP view for each detected client (Claude Code, Codex, Cursor, Gemini CLI, Hermes)
14
+ without storing a credential.
15
+ - Skill pack v2: the new `agentlinkops-connect` skill (views, verbs, evidence rules), per-client
16
+ `allowed-tools` and Codex `agents/openai.yaml` metadata, the plugin's `.mcp.json`, and a
17
+ discovery index with deterministic per-skill archives served at
18
+ `/.well-known/agent-skills/index.json`.
19
+ - Skill-first onboarding (DP-0037): `agentlinkops skill [--url]` prints the agent reference
20
+ that ships inside the connect skill (`references/agentlinkops.md`); the connect skill is a
21
+ stub that has the agent read it once per session; `init`, a terminal `tools` and
22
+ `agent setup` point at it once. The discovery index now follows the Agent Skills discovery
23
+ specification (`$schema`, `type: archive`) so `npx skills add https://agentlinkops.com`
24
+ installs the four product skills; the content pipeline sits behind
25
+ `https://agentlinkops.com/content`.
26
+ - `agentlinkops check --source URL --target URL [--out FILE]`: one accountless local check that
27
+ produces a portable result artifact, and `agentlinkops adopt-result FILE` to save that artifact
28
+ into a ledger once, validated and under the ledger writer lock.
29
+ - `agentlinkops setup --plan --goal verify-links|prepare-campaign|build-content [--mode …]`: a
30
+ read-only setup plan from metadata-only workspace inspection; no writes, network or account.
31
+ - `agentlinkops receipt history --performance FILE.json`: attaches dated Search Console and
32
+ optional GA4 context from the local context files beside a claim, without claiming causation.
33
+ - `node cli/context-mcp.js --root DIR`: the repository-local stdio MCP host for the eight
34
+ context tools. Adds the runtime dependency `@modelcontextprotocol/server` 2.0.0.
35
+
36
+ ## 0.4.0 (2026-09-15)
37
+
38
+ First public release under the Apache License 2.0.
39
+
40
+ - `agentlinkops` command: `init`, `add`, `check`, `status`, `diff`, `report`, `import`, `adopt`,
41
+ `sync`, `connect`, `doctor`, `migrate`, `tools`, `call`, `fleet`, `platforms`, `context`,
42
+ `mix`, `receive`, `receipt`, `fmt` and `compact`.
43
+ - `linktrail` alias: runs the same command with a one-line notice on stderr. The alias, the
44
+ `LINKTRAIL_*` environment variables and the `.linktrail/` ledger directory stop working on
45
+ 2026-10-15; `agentlinkops migrate` renames the directory.
46
+ - Shared verifier (`src/verifier/`): the module the hosted service runs, unchanged.
47
+ - Import adapters for Ahrefs, Semrush, Majestic, Moz, DataForSEO, Linkody, Google Search Console
48
+ and generic CSV, with an explicit `--map` for any other layout.
49
+ - Agent plugin manifests and skills (`agentlinkops-crm`, `agentlinkops-campaigns`,
50
+ `agentlinkops-assets` and the content-pipeline skills), campaign templates and the Python
51
+ CRM helper.
52
+ - Dependencies declared instead of vendored: parse5 8.0.1 and zod 4.6.0.
package/LICENSE ADDED
@@ -0,0 +1,190 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ Copyright 2026 AgentLinkOps
179
+
180
+ Licensed under the Apache License, Version 2.0 (the "License");
181
+ you may not use this file except in compliance with the License.
182
+ You may obtain a copy of the License at
183
+
184
+ http://www.apache.org/licenses/LICENSE-2.0
185
+
186
+ Unless required by applicable law or agreed to in writing, software
187
+ distributed under the License is distributed on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
+ See the License for the specific language governing permissions and
190
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,17 @@
1
+ AgentLinkOps CLI
2
+ Copyright 2026 AgentLinkOps
3
+
4
+ This product includes software developed by AgentLinkOps (https://agentlinkops.com/).
5
+
6
+ Licensed under the Apache License, Version 2.0. See LICENSE.
7
+
8
+ Third-party components redistributed with this package:
9
+
10
+ - skills/content-defingerprinting/scripts/watermarks/vendor/: files from
11
+ watermarks-remover (https://github.com/guillaumemeyer/watermarks-remover),
12
+ MIT License, Copyright (c) 2026 watermarks-remover contributors. The files are
13
+ byte-identical to the pinned upstream commit named in the sibling UPSTREAM.json;
14
+ the upstream license text is at vendor/LICENSE.upstream.
15
+
16
+ Runtime dependencies installed by npm and not redistributed here: parse5 (MIT),
17
+ zod (MIT).
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # agentlinkops
2
+
3
+ A backlink ledger that lives in your repository. `agentlinkops check` fetches a source page,
4
+ looks for the link you declared and records an observation with the HTTP status, redirect
5
+ chain, robots posture and a SHA-256 of the fetched bytes. Page HTML never enters the repository.
6
+ A check that cannot conclude says `unknown` with a named reason, and unknown never means the
7
+ link was removed.
8
+
9
+ The CLI imports the same verifier module the hosted AgentLinkOps service runs, and its tests
10
+ assert both paths return the same shape. The hosted checks, evidence store and MCP server are a
11
+ separate service; this package is the local half and needs no account.
12
+
13
+ ## Give your agent AgentLinkOps
14
+
15
+ Four steps, the same ones the docs and the landing page show. Node.js 22.13 or later.
16
+
17
+ 1. **Install the skill.** Detects the agent clients on this machine (Claude Code, Codex,
18
+ Cursor, Gemini CLI, Hermes), installs the skill pack at each one's skills path and writes
19
+ its MCP entry; stores no credential and prints what it changed.
20
+
21
+ ```sh
22
+ npx -y agentlinkops agent setup
23
+ ```
24
+
25
+ With the `skills` CLI instead: `npx -y skills add https://agentlinkops.com`, then add
26
+ `https://app.agentlinkops.com/mcp` as a remote HTTP MCP server in your client.
27
+ 2. **Sign in when your agent asks.** The client opens the AgentLinkOps sign-in; you pick the
28
+ account, workspace and scopes. Nothing is pasted into a chat.
29
+ 3. **Verify.** Ask your agent to list your AgentLinkOps projects, or run `agentlinkops doctor`.
30
+ 4. **Check a placement.** Ask: "check whether [publisher page] links to [my page], show the
31
+ dated observation and any uncertainty." No account yet? The same verifier runs locally:
32
+
33
+ ```sh
34
+ npx -y agentlinkops check --source https://publisher.example/resources --target https://your-site.example/guide
35
+ ```
36
+
37
+ The skill is a short door: the first time your agent uses AgentLinkOps in a session it reads
38
+ the full agent reference, which `agentlinkops skill` prints and
39
+ https://agentlinkops.com/SKILL.md serves. Hosted monitoring is an invitation-only pilot; the
40
+ local commands below need no account.
41
+
42
+ ## Install the CLI on its own
43
+
44
+ ```sh
45
+ npm install -g agentlinkops
46
+ agentlinkops --help
47
+ ```
48
+
49
+ Or run it once without installing:
50
+
51
+ ```sh
52
+ npx agentlinkops check
53
+ ```
54
+
55
+ ## Check one link with no account
56
+
57
+ ```sh
58
+ cd your-repository
59
+ agentlinkops init
60
+ agentlinkops add --source https://publisher.example.com/resources --target https://example.com/guide --intent expected
61
+ agentlinkops check
62
+ agentlinkops status
63
+ ```
64
+
65
+ `init` creates `.agentlinkops/` with an empty ledger. `add` writes one entry. `check` fetches
66
+ the source page and appends an observation to the mirror. `status` names every entry whose
67
+ observations disagree with its intent and proposes the edit; applying it is a commit you make.
68
+
69
+ Exit codes: `0` when every expected link is present, `1` only when an expected link is observed
70
+ absent with complete evidence, `2` for a usage, configuration or ledger error. An unknown never
71
+ fails the run. Pass `--fail-on-unknown` when your CI wants it to.
72
+
73
+ `agentlinkops doctor` verifies the ledger, the verifier self-test, cloud reachability and the
74
+ token, and prints a fix line for anything that fails.
75
+
76
+ ## The ledger
77
+
78
+ Intent and fact never share a file. You write intent; the verifier writes fact.
79
+
80
+ ```
81
+ .agentlinkops/
82
+ config.json project identity, cloud origin, defaults. Yours, committed
83
+ links.jsonl the ledger: intent. Yours, committed
84
+ observations.jsonl fact mirror, tool-owned, committed
85
+ events.jsonl cloud event mirror by cursor, tool-owned, committed
86
+ candidates.jsonl imported and discovered rows, tool-owned, committed
87
+ state.json cursors and last-check marks, tool-owned, committed
88
+ ```
89
+
90
+ One ledger entry:
91
+
92
+ ```json
93
+ {"id":"lk_9f3ac1","intent":"expected","source":"https://publisher.example.com/resources","target":"https://example.com/guide","scope":"exact","expect":{"anchor":"guide","rel":["nofollow"]},"cadence":"daily","ref":"q3-resource-pages/441","tags":["resource-page"],"added":"2026-09-11","note":"editor confirmed"}
94
+ ```
95
+
96
+ Three intents: `wanted` (we want this publisher to link to us; present is the news),
97
+ `expected` (it should be there now; absent is the alarm) and `retired` (stop checking, keep
98
+ the history). Nothing promotes an entry automatically. `id` is random and permanent, so a
99
+ corrected URL keeps its history. Everything is plain JSON Lines, one entry per line, that you
100
+ can read without this tool.
101
+
102
+ ## Imports and adoption
103
+
104
+ Supplier exports import through presets or an explicit column map, and a preview writes nothing:
105
+
106
+ ```sh
107
+ agentlinkops import ahrefs-export.csv --target example.com --from ahrefs
108
+ agentlinkops import any-export.csv --target example.com --map "source=Referring page URL,target=Target URL"
109
+ ```
110
+
111
+ Presets: Ahrefs, Semrush, Majestic, Moz, DataForSEO, Linkody, Google Search Console and generic
112
+ CSV. A preset is a convenience over `--map`, never a requirement. Every rejected row is reported
113
+ with its line and reason. Count-per-site Search Console exports are refused, because importing
114
+ them would invent placements.
115
+
116
+ An existing SQLite CRM (the reference adapter in `scripts/agentlinkops.py`) adopts into ledger
117
+ entries read-only with `agentlinkops adopt crm.sqlite`; there is no reverse direction.
118
+
119
+ One page can be checked without a ledger or an account: `agentlinkops check --source URL
120
+ --target URL [--scope exact] [--json] [--out result.json]` runs the same verifier and prints one
121
+ result. `--out` reserves the file before the fetch and refuses to overwrite. A saved result later
122
+ joins a ledger with `agentlinkops adopt-result result.json [--intent wanted|expected]`, which
123
+ validates the artifact (no raw HTML, a checksum that must match, a state consistent with its
124
+ evidence) and appends the entry and observation once under a writer lock.
125
+
126
+ ## Sync with the hosted service
127
+
128
+ Local commands need no account. Sync pushes expectations and pulls events and evidence
129
+ references into the mirror. It never writes into `links.jsonl`.
130
+
131
+ ```sh
132
+ export AGENTLINKOPS_TOKEN=... # from Agent access > Create API key in the hosted workspace
133
+ agentlinkops connect --workspace WORKSPACE_ID --project-id PROJECT_ID --origin https://app.agentlinkops.com
134
+ agentlinkops sync --dry-run
135
+ agentlinkops sync
136
+ ```
137
+
138
+ Hosted access is an invitation-only pilot. The hosted checks, evidence store and MCP server are a
139
+ separate service and are not part of this package.
140
+
141
+ ## Citation watches
142
+
143
+ An AI citation is a placement: a reference to your asset on a surface you do not control,
144
+ re-decided by the model on every ask. So the CLI watches it the way it watches a link — a prompt
145
+ panel, run against engine APIs on cadence, with dated evidence and honest statistics. A citation
146
+ is never reported as "removed": each epoch states the citation rate with a Wilson interval and
147
+ its sample size, classified against the previous epoch as declined, grown or
148
+ not_distinguishable, or `insufficient_data` when n is too thin to interpret.
149
+
150
+ ```sh
151
+ export PERPLEXITY_API_KEY=... # live engine; omit it and the mock engine runs at $0
152
+ agentlinkops citation run panel.json --max-usd 1.00
153
+ ```
154
+
155
+ The panel is JSON: `targets` (domain or url, brand, aliases), `prompts`, `engines`
156
+ (`mock` or `perplexity`), optional `samples` and `maxUsd`. Every run writes immutable
157
+ content-addressed answer snapshots plus append-only observation and epoch rows under
158
+ `.agentlinkops/citations/`. The USD cap is checked before each call, so it can be approached
159
+ but never exceeded, and a budget abort leaves a partial-epoch receipt and exit code 2. A cell
160
+ that declined with complete evidence exits 1, like an expected link observed absent.
161
+
162
+ ## Environment variables
163
+
164
+ | Variable | Purpose |
165
+ | --- | --- |
166
+ | `AGENTLINKOPS_TOKEN` or `AGENTLINKOPS_API_KEY` | Scoped credential for `connect`, `sync`, `tools`, `call` and the token probe in `doctor`. If both are set they must agree. |
167
+ | `AGENTLINKOPS_API_URL` | Hosted origin when it is not in `config.json`. |
168
+ | `AGENTLINKOPS_WEBHOOK_SECRET` | Verifies signed deliveries in `agentlinkops receive`. |
169
+ | `AGENTLINKOPS_GSC_TOKEN`, `AGENTLINKOPS_GA4_TOKEN` | First-party context commands under `agentlinkops context`. |
170
+ | `PERPLEXITY_API_KEY` | Live engine credential for `agentlinkops citation`. Presence is reported by `doctor`; the value is never printed. |
171
+
172
+ No value is ever printed. The `LINKTRAIL_*` spellings, the `linktrail` command and a `.linktrail/`
173
+ ledger directory are still accepted with a one-line notice on stderr until 2026-10-15; run
174
+ `agentlinkops migrate` once to rename the directory. It refuses to merge or delete anything.
175
+
176
+ ## Agent plugin
177
+
178
+ The package doubles as a plugin: `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`
179
+ point at `skills/`, which holds the connect, CRM, campaign and asset skills plus the
180
+ content-pipeline skills, with templates in `templates/` and contracts in `references/`.
181
+ `agentlinkops agent setup` installs the same skills for any detected client, and the discovery
182
+ index at `https://agentlinkops.com/.well-known/agent-skills/index.json` serves them to
183
+ `npx skills add` (the content pipeline sits behind `https://agentlinkops.com/content`). Node.js
184
+ runs the ledger CLI; Python 3.10 or later runs the optional CRM helper and the watermark
185
+ scanner.
186
+
187
+ ## What it does not do
188
+
189
+ It holds no backlink index, sends no email, verifies no contact address and runs no JavaScript:
190
+ observations are static HTML only, and each one says so. Literal IP addresses, reserved hostnames
191
+ and non-default ports are refused before any fetch.
192
+
193
+ ## Documentation and source
194
+
195
+ - Guides: https://docs.agentlinkops.com/guides/first-result and https://docs.agentlinkops.com/guides/connect-cli
196
+ - Product: https://agentlinkops.com/
197
+ - Source: https://github.com/sunlover7/agentlinkops-cli
198
+ - Contract references shipped in this package: `references/cli.md`, `references/disavow.md`, `references/reports.md`
199
+
200
+ ## License
201
+
202
+ Apache License 2.0. See `LICENSE` and `NOTICE`.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ // The `agentlinkops` executable. `linktrail.mjs` beside it is the compatibility alias.
3
+ import { main } from '../cli/main.js';
4
+ process.exitCode = await main();
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // Compatibility alias for the rename window: the executable is `agentlinkops`. The alias stops
3
+ // working on 2026-10-15. One line to stderr, then the same main; stdout stays clean for --json readers.
4
+ import { main } from '../cli/main.js';
5
+ process.stderr.write('linktrail: this command is now `agentlinkops`; the old name keeps working during the pilot compatibility window.\n');
6
+ process.exitCode = await main();
@@ -0,0 +1,68 @@
1
+ // A dependency-free RFC 4180 reader.
2
+ //
3
+ // Written rather than depended on because the CLI is meant to run anywhere with no install, and
4
+ // because the failure that matters here is silent: a parser that mishandles a quoted field
5
+ // containing a comma does not throw, it shifts every column right and imports nonsense.
6
+ export class CsvError extends Error {
7
+ constructor(message, line) { super(message); this.name = 'CsvError'; this.line = line; this.exitCode = 2; }
8
+ }
9
+
10
+ /** Rows of raw string cells. Quoted fields may contain commas, quotes and newlines. */
11
+ export function parseCsv(text, { delimiter = ',' } = {}) {
12
+ // A BOM at the start of a spreadsheet export becomes part of the first header name, and then
13
+ // the first column never matches anything. Excel writes one by default.
14
+ const input = text.replace(/^/u, '');
15
+ const rows = [];
16
+ let row = [], field = '', quoted = false, line = 1, started = false;
17
+ for (let i = 0; i < input.length; i++) {
18
+ const char = input[i];
19
+ if (quoted) {
20
+ if (char === '"') {
21
+ if (input[i + 1] === '"') { field += '"'; i++; continue; }
22
+ quoted = false; continue;
23
+ }
24
+ if (char === '\n') line++;
25
+ field += char;
26
+ continue;
27
+ }
28
+ if (char === '"' && field === '') { quoted = true; started = true; continue; }
29
+ if (char === delimiter) { row.push(field); field = ''; started = false; continue; }
30
+ if (char === '\r') continue;
31
+ if (char === '\n') {
32
+ line++;
33
+ if (row.length || field !== '' || started) { row.push(field); rows.push(row); }
34
+ row = []; field = ''; started = false;
35
+ continue;
36
+ }
37
+ field += char; started = true;
38
+ }
39
+ if (quoted) throw new CsvError('the file ends inside a quoted field', line);
40
+ if (row.length || field !== '' || started) { row.push(field); rows.push(row); }
41
+ return rows;
42
+ }
43
+
44
+ /** Normalised for matching: case, punctuation and spacing are not information here. */
45
+ export const normalizeHeader = name => String(name).toLowerCase().replace(/[\s_\-.()/]+/gu, '').trim();
46
+
47
+ /**
48
+ * Header row plus objects keyed by the ORIGINAL header, so an unmapped column keeps the name
49
+ * the customer's tool gave it.
50
+ */
51
+ export function readTable(text, options = {}) {
52
+ const rows = parseCsv(text, options);
53
+ if (!rows.length) throw new CsvError('the file is empty', 1);
54
+ const header = rows[0].map(cell => cell.trim());
55
+ if (!header.some(Boolean)) throw new CsvError('the first row is blank, so there are no column names', 1);
56
+ const records = [];
57
+ rows.slice(1).forEach((cells, index) => {
58
+ // A short or long row is a REPORT, not a silent pad or truncate: it usually means the file
59
+ // is not the delimiter we think it is, and padding hides that until the numbers are wrong.
60
+ if (cells.length !== header.length && cells.some(Boolean)) {
61
+ records.push({ line: index + 2, error: `${cells.length} value(s) for ${header.length} column(s)` });
62
+ return;
63
+ }
64
+ if (!cells.some(Boolean)) return;
65
+ records.push({ line: index + 2, values: Object.fromEntries(header.map((name, position) => [name, cells[position] ?? ''])) });
66
+ });
67
+ return { header, records };
68
+ }
@@ -0,0 +1,79 @@
1
+ // The SQLite CRM as one adapter, not the local surface.
2
+ //
3
+ // The toolkit's six-table CRM works, is tested, and fits an agency that wants contacts and
4
+ // outreach activity in the same place. What it is no longer is the ONLY way to keep links
5
+ // locally — the ledger file is — so this reads a CRM into ledger entries and leaves the CRM
6
+ // untouched. **Read-only, and no new required schema**: the adapter adapts to what is there.
7
+ //
8
+ // Adopting an existing CRM is a MIGRATION rather than a discovery import, which is why it is
9
+ // `adopt` and not `import`. An import brings in a supplier's index of backlinks the customer has
10
+ // not decided about; a CRM holds decisions they already made.
11
+ import { DatabaseSync } from 'node:sqlite';
12
+ import { createHash } from 'node:crypto';
13
+ import { normalizeEntry } from '../ledger.js';
14
+
15
+ export class CrmError extends Error {
16
+ constructor(message) { super(message); this.name = 'CrmError'; this.exitCode = 2; }
17
+ }
18
+
19
+ // Words that mean "stop watching this". Everything else keeps the default for its table,
20
+ // because `status` in this CRM is free text and refusing every unrecognised value would refuse
21
+ // nearly every row. What the adapter does instead is REPORT the statuses it saw.
22
+ const RETIRING = new Set(['rejected', 'declined', 'dead', 'lost', 'removed', 'abandoned', 'closed-lost', 'disavowed']);
23
+
24
+ const idFor = (table, value) => `lk_${createHash('sha256').update(`crm:${table}:${value}`).digest('hex').slice(0, 10)}`;
25
+
26
+ export function readCrm(path, { defaultScope = 'exact' } = {}) {
27
+ let db;
28
+ try { db = new DatabaseSync(path, { readOnly: true }); }
29
+ catch (error) { throw new CrmError(`could not open ${path}: ${error.message}`); }
30
+ try {
31
+ const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
32
+ const missing = ['placements', 'opportunities'].filter(name => !tables.has(name));
33
+ if (missing.length === 2) throw new CrmError(`${path} has no placements or opportunities table, so it is not an AgentLinkOps CRM`);
34
+
35
+ const entries = [], unmapped = [], statuses = { placement: {}, opportunity: {} };
36
+ const placementSources = new Set();
37
+
38
+ const add = (table, row, intent) => {
39
+ try {
40
+ entries.push(normalizeEntry({
41
+ id: idFor(table, row.id), intent,
42
+ source: row.source_url, target: row.target_url, scope: defaultScope,
43
+ ref: `crm/${table}/${row.id}`,
44
+ ...(row.notes ? { note: String(row.notes).slice(0, 200) } : {}),
45
+ added: (row.created_at ?? '').slice(0, 10) || undefined,
46
+ }, { assignId: false }));
47
+ } catch (error) { unmapped.push({ table, id: row.id, reason: error.message }); }
48
+ };
49
+
50
+ if (tables.has('placements')) {
51
+ for (const row of db.prepare('SELECT * FROM placements').all()) {
52
+ const status = String(row.status ?? '').trim().toLowerCase();
53
+ statuses.placement[status || '(blank)'] = (statuses.placement[status || '(blank)'] ?? 0) + 1;
54
+ // A placement is a link the customer believes exists, so it is EXPECTED — absence is the
55
+ // alarm. That is the whole difference between the two intents.
56
+ add('placements', row, RETIRING.has(status) ? 'retired' : 'expected');
57
+ if (row.opportunity_id) placementSources.add(row.opportunity_id);
58
+ }
59
+ }
60
+ if (tables.has('opportunities')) {
61
+ for (const row of db.prepare('SELECT * FROM opportunities').all()) {
62
+ // An opportunity that became a placement is superseded by it; carrying both would watch
63
+ // one link twice and report it twice.
64
+ if (placementSources.has(row.id)) continue;
65
+ const status = String(row.status ?? '').trim().toLowerCase();
66
+ statuses.opportunity[status || '(blank)'] = (statuses.opportunity[status || '(blank)'] ?? 0) + 1;
67
+ add('opportunities', row, RETIRING.has(status) ? 'retired' : 'wanted');
68
+ }
69
+ }
70
+ return {
71
+ entries, unmapped, statuses,
72
+ // Named so an operator can see what was ASSUMED active rather than discovering it later.
73
+ assumed_active: {
74
+ placement: Object.keys(statuses.placement).filter(status => !RETIRING.has(status)),
75
+ opportunity: Object.keys(statuses.opportunity).filter(status => !RETIRING.has(status)),
76
+ },
77
+ };
78
+ } finally { db?.close(); }
79
+ }