agentlinkops 0.6.11 → 0.6.16

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 (311) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +2 -2
  3. package/.mcp.json +0 -0
  4. package/CHANGELOG.md +35 -0
  5. package/LICENSE +0 -0
  6. package/NOTICE +0 -0
  7. package/PACK-ARTIFACT.json +16 -0
  8. package/PACKS.json +591 -0
  9. package/README.md +34 -48
  10. package/bin/agentlinkops.mjs +0 -0
  11. package/bin/linktrail.mjs +0 -0
  12. package/cli/adapters/csv.js +0 -0
  13. package/cli/adapters/sqlite-crm.js +0 -0
  14. package/cli/adapters/suppliers.js +1 -1
  15. package/cli/admission.js +0 -0
  16. package/cli/agent-setup.js +101 -17
  17. package/cli/agentlinkops.mjs +0 -0
  18. package/cli/args.js +0 -0
  19. package/cli/campaign-bridge.js +325 -0
  20. package/cli/campaign-outcomes.js +107 -0
  21. package/cli/catalog.js +47686 -16398
  22. package/cli/check.js +40 -10
  23. package/cli/chronology.js +39 -0
  24. package/cli/citation.js +0 -0
  25. package/cli/client.js +2 -1
  26. package/cli/cloud-error.js +0 -0
  27. package/cli/commands.js +17 -8
  28. package/cli/config.js +0 -0
  29. package/cli/connection.js +92 -18
  30. package/cli/contact-adoption.js +91 -0
  31. package/cli/context-mcp.js +0 -0
  32. package/cli/context.js +13 -2
  33. package/cli/delivery.js +0 -0
  34. package/cli/disavow.js +0 -0
  35. package/cli/doctor.js +15 -4
  36. package/cli/env.js +0 -0
  37. package/cli/fleet.js +30 -19
  38. package/cli/gsc-browser.js +0 -0
  39. package/cli/import.js +7 -1
  40. package/cli/index-observations.js +0 -0
  41. package/cli/index-submissions.js +0 -0
  42. package/cli/key-store.js +115 -0
  43. package/cli/ledger.js +1 -0
  44. package/cli/lifecycle.js +0 -0
  45. package/cli/linktrail.mjs +0 -0
  46. package/cli/local-result.js +41 -6
  47. package/cli/main.js +85 -20
  48. package/cli/mcp-client.js +2 -2
  49. package/cli/migration.js +308 -0
  50. package/cli/mirror.js +0 -0
  51. package/cli/mix.js +0 -0
  52. package/cli/native-lock.js +0 -0
  53. package/cli/oauth-credentials.js +0 -0
  54. package/cli/oauth-flow.js +0 -0
  55. package/cli/placement-projection.js +106 -0
  56. package/cli/platforms.js +0 -0
  57. package/cli/prompt.js +25 -0
  58. package/cli/public-http.js +168 -0
  59. package/cli/receipt-performance.js +0 -0
  60. package/cli/receipts.js +0 -0
  61. package/cli/report.js +51 -19
  62. package/cli/setup-plan.js +0 -0
  63. package/cli/skill.js +0 -0
  64. package/cli/state.js +76 -29
  65. package/cli/status.js +14 -18
  66. package/cli/sync-lock.js +0 -0
  67. package/cli/sync.js +23 -4
  68. package/package.json +7 -8
  69. package/references/citation-panels.md +2 -0
  70. package/references/cli.md +2 -2
  71. package/references/disavow.md +1 -1
  72. package/references/discovery-handoff.md +1 -1
  73. package/references/local-crm-draft-handoff.md +1 -1
  74. package/references/portable-ledger-handoff.md +11 -10
  75. package/references/recipes/catalog.json +263 -7
  76. package/references/recipes/command-notes.md +5 -3
  77. package/references/recipes/internal-link-improvement.md +25 -0
  78. package/references/recipes/placement-reconciliation.md +53 -1
  79. package/references/recipes/qualified-campaign-handoff.md +0 -0
  80. package/references/recipes/site-context-brief.md +0 -0
  81. package/references/recipes/sourced-linkable-asset.md +0 -0
  82. package/references/reports.md +0 -0
  83. package/scripts/agentlinkops.py +0 -0
  84. package/scripts/disavow.py +0 -0
  85. package/scripts/linktrail.py +0 -0
  86. package/scripts/migrations/001_crm.sql +0 -0
  87. package/scripts/migrations/002_cloud_events.sql +0 -0
  88. package/scripts/migrations/003_disavow.sql +0 -0
  89. package/scripts/migrations/004_placement_costs.sql +0 -0
  90. package/scripts/outreach-crm-draft.py +0 -0
  91. package/scripts/portable-handoff.py +17 -4
  92. package/scripts/report.py +0 -0
  93. package/shared/admission-contract.js +3 -2
  94. package/shared/admission-report-contract.js +3 -2
  95. package/shared/agent-clients.js +0 -0
  96. package/shared/analytics-history-contract.js +15 -0
  97. package/shared/campaign-outcome-contract.js +12 -0
  98. package/shared/campaign-prospect-contract.js +23 -0
  99. package/shared/citation-evidence-contract.js +0 -0
  100. package/shared/classification-contract.js +54 -0
  101. package/shared/contact-record-contract.js +27 -0
  102. package/shared/disavow-contract.js +37 -9
  103. package/shared/exec-grammar.js +0 -0
  104. package/shared/index-observation-contract.js +5 -4
  105. package/shared/internal-links-contract.js +143 -0
  106. package/shared/lifecycle-contract.js +0 -0
  107. package/shared/lifecycle-money.js +0 -0
  108. package/shared/link-category-contract.js +35 -0
  109. package/shared/link-category.js +138 -0
  110. package/shared/link-labels.js +26 -0
  111. package/shared/link-summary-contract.js +17 -0
  112. package/shared/monitoring-chronology-export.js +57 -0
  113. package/shared/monitoring-read-contract.js +41 -0
  114. package/shared/observation-event.js +1 -1
  115. package/shared/onboarding.js +20 -0
  116. package/shared/opportunity-record.js +0 -0
  117. package/shared/response-contract.js +41 -5
  118. package/shared/search-connection-contract.js +2 -1
  119. package/shared/table-page-contract.js +13 -0
  120. package/skills/agentlinkops-assets/SKILL-DEPS.json +290 -0
  121. package/skills/agentlinkops-assets/SKILL.md +3 -1
  122. package/skills/agentlinkops-assets/agents/openai.yaml +1 -1
  123. package/skills/agentlinkops-assets/references/portable/references/cli.md +220 -0
  124. package/skills/agentlinkops-assets/references/portable/references/disavow.md +73 -0
  125. package/skills/agentlinkops-assets/references/portable/references/reports.md +53 -0
  126. package/skills/agentlinkops-assets/references/portable/scripts/agentlinkops.py +676 -0
  127. package/skills/agentlinkops-assets/references/portable/scripts/disavow.py +207 -0
  128. package/skills/agentlinkops-assets/references/portable/scripts/migrations/001_crm.sql +45 -0
  129. package/skills/agentlinkops-assets/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  130. package/skills/agentlinkops-assets/references/portable/scripts/migrations/003_disavow.sql +20 -0
  131. package/skills/agentlinkops-assets/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  132. package/skills/agentlinkops-assets/references/portable/scripts/report.py +113 -0
  133. package/skills/agentlinkops-assets/references/portable/templates/campaigns.v1.json +65 -0
  134. package/skills/agentlinkops-campaigns/SKILL-DEPS.json +336 -0
  135. package/skills/agentlinkops-campaigns/SKILL.md +7 -5
  136. package/skills/agentlinkops-campaigns/agents/openai.yaml +1 -1
  137. package/skills/agentlinkops-campaigns/references/first-campaign.md +31 -0
  138. package/skills/agentlinkops-campaigns/references/mailbox-campaign.md +46 -0
  139. package/skills/agentlinkops-campaigns/references/portable/references/cli.md +220 -0
  140. package/skills/agentlinkops-campaigns/references/portable/references/disavow.md +73 -0
  141. package/skills/agentlinkops-campaigns/references/portable/references/reports.md +53 -0
  142. package/skills/agentlinkops-campaigns/references/portable/scripts/agentlinkops.py +676 -0
  143. package/skills/agentlinkops-campaigns/references/portable/scripts/disavow.py +207 -0
  144. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/001_crm.sql +45 -0
  145. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  146. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/003_disavow.sql +20 -0
  147. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  148. package/skills/agentlinkops-campaigns/references/portable/scripts/report.py +113 -0
  149. package/skills/agentlinkops-campaigns/references/portable/templates/campaigns.v1.json +65 -0
  150. package/skills/agentlinkops-connect/SKILL-DEPS.json +104 -0
  151. package/skills/agentlinkops-connect/SKILL.md +2 -0
  152. package/skills/agentlinkops-connect/agents/openai.yaml +1 -1
  153. package/skills/agentlinkops-connect/references/agentlinkops.md +164 -61
  154. package/skills/agentlinkops-connect/references/placement-workflows.md +19 -0
  155. package/skills/agentlinkops-crm/SKILL-DEPS.json +381 -0
  156. package/skills/agentlinkops-crm/SKILL.md +26 -9
  157. package/skills/agentlinkops-crm/agents/openai.yaml +1 -1
  158. package/skills/agentlinkops-crm/references/portable/references/cli.md +220 -0
  159. package/skills/agentlinkops-crm/references/portable/references/disavow.md +73 -0
  160. package/skills/agentlinkops-crm/references/portable/references/local-crm-draft-handoff.md +76 -0
  161. package/skills/agentlinkops-crm/references/portable/references/portable-ledger-handoff.md +51 -0
  162. package/skills/agentlinkops-crm/references/portable/references/reports.md +53 -0
  163. package/skills/agentlinkops-crm/references/portable/scripts/agentlinkops.py +676 -0
  164. package/skills/agentlinkops-crm/references/portable/scripts/disavow.py +207 -0
  165. package/skills/agentlinkops-crm/references/portable/scripts/migrations/001_crm.sql +45 -0
  166. package/skills/agentlinkops-crm/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  167. package/skills/agentlinkops-crm/references/portable/scripts/migrations/003_disavow.sql +20 -0
  168. package/skills/agentlinkops-crm/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  169. package/skills/agentlinkops-crm/references/portable/scripts/outreach-crm-draft.py +121 -0
  170. package/skills/agentlinkops-crm/references/portable/scripts/portable-handoff.py +197 -0
  171. package/skills/agentlinkops-crm/references/portable/scripts/report.py +113 -0
  172. package/skills/agentlinkops-crm/references/portable/templates/campaigns.v1.json +65 -0
  173. package/skills/agentlinkops-discovery/SKILL-DEPS.json +301 -0
  174. package/skills/agentlinkops-discovery/SKILL.md +4 -2
  175. package/skills/agentlinkops-discovery/references/portable/references/cli.md +220 -0
  176. package/skills/agentlinkops-discovery/references/portable/references/disavow.md +73 -0
  177. package/skills/agentlinkops-discovery/references/portable/references/discovery-handoff.md +32 -0
  178. package/skills/agentlinkops-discovery/references/portable/references/reports.md +53 -0
  179. package/skills/agentlinkops-discovery/references/portable/scripts/agentlinkops.py +676 -0
  180. package/skills/agentlinkops-discovery/references/portable/scripts/disavow.py +207 -0
  181. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/001_crm.sql +45 -0
  182. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  183. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/003_disavow.sql +20 -0
  184. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  185. package/skills/agentlinkops-discovery/references/portable/scripts/report.py +113 -0
  186. package/skills/agentlinkops-discovery/references/portable/templates/campaigns.v1.json +65 -0
  187. package/skills/agentlinkops-discovery/references/saved-backlink-workflows.md +23 -0
  188. package/src/citations/adapter.js +0 -0
  189. package/src/citations/alert.js +0 -0
  190. package/src/citations/browser/chatgpt-citations.js +0 -0
  191. package/src/citations/browser/engine.js +0 -0
  192. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
  193. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
  194. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
  195. package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
  196. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
  197. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
  198. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
  199. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
  200. package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
  201. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
  202. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
  203. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
  204. package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
  205. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
  206. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
  207. package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
  208. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
  209. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
  210. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
  211. package/src/citations/browser/gen/core/providers/index.js +0 -0
  212. package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
  213. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
  214. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
  215. package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
  216. package/src/citations/browser/gen/lib/browser/display.js +0 -0
  217. package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
  218. package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
  219. package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
  220. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
  221. package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
  222. package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
  223. package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
  224. package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
  225. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
  226. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
  227. package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
  228. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
  229. package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
  230. package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
  231. package/src/citations/browser/gen/utils/agent-constants.js +0 -0
  232. package/src/citations/browser/providers.js +0 -0
  233. package/src/citations/browser/proxy-admission.js +0 -0
  234. package/src/citations/browser/shims/errors.js +0 -0
  235. package/src/citations/browser/shims/types.js +0 -0
  236. package/src/citations/browser/shims/utils.js +0 -0
  237. package/src/citations/contract.js +0 -0
  238. package/src/citations/egress-receipts.js +0 -0
  239. package/src/citations/evidence-integrity.js +0 -0
  240. package/src/citations/inventory.js +0 -0
  241. package/src/citations/login.js +0 -0
  242. package/src/citations/match.js +0 -0
  243. package/src/citations/panel-builder.js +0 -0
  244. package/src/citations/report.js +0 -0
  245. package/src/citations/runner.js +0 -0
  246. package/src/citations/stats.js +0 -0
  247. package/src/citations/sync.js +0 -0
  248. package/src/citations/watch.js +0 -0
  249. package/src/competitors/contract.js +17 -1
  250. package/src/competitors/domain-mix.js +0 -0
  251. package/src/context/ga4.js +0 -0
  252. package/src/context/gsc.js +0 -0
  253. package/src/context/site-profile.js +34 -121
  254. package/src/context/util.js +0 -0
  255. package/src/context-tool-registry.js +0 -0
  256. package/src/corpus/acquire.js +68 -32
  257. package/src/corpus/extract.js +147 -0
  258. package/src/corpus/frontier.js +151 -0
  259. package/src/corpus/seeds.js +0 -0
  260. package/src/discovery/contract.js +8 -0
  261. package/src/discovery/import.js +13 -1
  262. package/src/evidence-reference.js +0 -0
  263. package/src/index-observation.js +0 -0
  264. package/src/locator.js +0 -0
  265. package/src/mentions.js +193 -0
  266. package/src/page-acquisition/acquire.js +50 -0
  267. package/src/page-acquisition/bytes.js +138 -0
  268. package/src/page-acquisition/capture.js +11 -0
  269. package/src/page-acquisition/contracts.js +67 -0
  270. package/src/page-acquisition/documents.js +166 -0
  271. package/src/page-acquisition/http.js +307 -0
  272. package/src/page-acquisition/policy.js +20 -0
  273. package/src/page-acquisition/recovery.js +164 -0
  274. package/src/page-acquisition/render-failure.js +82 -0
  275. package/src/page-extraction/html.js +359 -0
  276. package/src/page-extraction/index.js +30 -0
  277. package/src/page-extraction/metadata.js +16 -0
  278. package/src/response-shaping.js +4 -3
  279. package/src/tool-views.js +23 -12
  280. package/src/verifier/fetch.js +4 -339
  281. package/src/verifier/index.js +81 -23
  282. package/src/verifier/observation-kind.js +0 -0
  283. package/src/verifier/page-quality.js +14 -0
  284. package/src/verifier/parser.js +2 -241
  285. package/src/verifier/readiness.js +64 -13
  286. package/src/verifier/render-evidence.js +25 -2
  287. package/src/verifier/robots.js +0 -0
  288. package/src/verifier/target.js +201 -0
  289. package/src/verifier/transition.js +18 -3
  290. package/src/verifier/url.js +31 -0
  291. package/src/watch-crawlability.js +144 -0
  292. package/templates/SITE.template.md +0 -0
  293. package/templates/campaigns.v1.json +0 -0
  294. package/skills/content-defingerprinting/SKILL.md +0 -29
  295. package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -102
  296. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -26
  297. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -52
  298. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -21
  299. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -96
  300. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -432
  301. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -86
  302. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -460
  303. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -651
  304. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -27
  305. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -212
  306. package/skills/internal-linking-optimizer/SKILL.md +0 -56
  307. package/skills/internal-linking-optimizer/references/source-prompt.md +0 -76
  308. package/skills/pre-publish-review/SKILL.md +0 -179
  309. package/skills/source-cited-content-builder/SKILL.md +0 -56
  310. package/skills/source-cited-content-builder/references/source-prompt.md +0 -19
  311. package/skills/source-cited-humanizer/SKILL.md +0 -20
package/cli/fleet.js CHANGED
@@ -6,54 +6,62 @@
6
6
  // and a summary that deduplicated them would silently pick one. So every number below is
7
7
  // per project, the only cross-project arithmetic is a sum of counts, and the output says so.
8
8
  //
9
- // "Earned" follows the ledger contract, not a status column: an entry is earned when its
10
- // latest OBSERVATION says present. A project with no observation file has earned nothing and
11
- // is reported as never checked rather than as zero links.
9
+ // "Earned" follows known conclusive presence; a later inconclusive attempt is disclosed
10
+ // separately. Activity is supplied explicitly per project, never discovered in another repo.
12
11
  import { readLedger } from './ledger.js';
13
- import { readObservations, latestByEntry } from './mirror.js';
12
+ import { readObservations } from './mirror.js';
13
+ import { readState } from './state.js';
14
+ import { projectPlacements } from './placement-projection.js';
14
15
 
15
16
  const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
16
17
 
17
18
  /** One project's row. Everything is computed inside the project; nothing crosses the boundary. */
18
- export async function fleetProject(name, ledgerPath, observationsPath) {
19
+ export async function fleetProject(name, ledgerPath, observationsPath, statePath = null) {
19
20
  const ledger = await readLedger(ledgerPath);
20
21
  if (ledger.missing) { const error = new Error(`no ledger at ${ledgerPath}`); error.code = 'NO_LEDGER'; throw error; }
21
22
  const byIntent = { wanted: 0, expected: 0, retired: 0 };
22
23
  for (const entry of ledger.entries) byIntent[entry.intent] = (byIntent[entry.intent] ?? 0) + 1;
23
24
 
24
25
  const { rows } = observationsPath ? await readObservations(observationsPath) : { rows: [] };
25
- const latest = latestByEntry(rows);
26
- let earned = 0, observed = 0;
27
- for (const [id, row] of latest) {
28
- if (!ledger.entries.some(entry => entry.id === id)) continue; // observations from another ledger's ids
29
- observed++;
30
- if (row.state === 'present') earned++;
31
- }
32
- const lastCheck = rows.length ? rows.map(row => row.checked_at).sort().at(-1) : null;
26
+ const state = statePath ? await readState(statePath) : null;
27
+ const eligible = ledger.entries.filter(entry => entry.intent !== 'retired');
28
+ const projected = projectPlacements(eligible, rows, { state });
29
+ const observed = projected.filter(item => item.latest_attempt).length;
30
+ const earned = projected.filter(item => item.current_state === 'present').length;
31
+ const newest = values => values.filter(Boolean).sort((a, b) => Date.parse(a) - Date.parse(b)).at(-1) ?? null;
33
32
  return {
34
33
  project: name, ledger: ledgerPath,
35
34
  entries: ledger.entries.length, ...byIntent,
36
35
  problems: ledger.problems.length,
37
36
  observed_entries: observed, earned,
38
- never_checked: ledger.entries.filter(entry => entry.intent !== 'retired').length - observed,
39
- last_observation: lastCheck,
37
+ never_checked: eligible.length - observed,
38
+ last_observation: newest(projected.map(item => item.evidence_observed_at)),
39
+ last_attempt_at: newest(projected.map(item => item.latest_attempt?.checked_at)),
40
+ last_success_at: newest(projected.map(item => item.last_successful_observation?.checked_at)),
41
+ last_link_verification_at: newest(projected.map(item => item.last_link_verification?.checked_at)),
42
+ uncertain_entries: projected.filter(item => item.uncertain).length,
43
+ activity_available: projected.some(item => item.activity_available),
44
+ activity_entries: projected.filter(item => item.activity_available).length,
45
+ coverage: { denominator: 'non_retired_ledger_entries', eligible_entries: eligible.length,
46
+ identity_unmatched_entries: projected.filter(item => item.identity_status === 'not_matched').length,
47
+ activity_input: statePath ? 'explicit_state_file' : 'not_supplied' },
40
48
  // Due volume, the same arithmetic the dogfood records use to price a fleet.
41
49
  implied_weekly_checks: byIntent.wanted + byIntent.expected * 7,
42
50
  };
43
51
  }
44
52
 
45
53
  /** Summons the fleet. Same numbers per project, one sum of counts, no merged intent. */
46
- export async function fleetSummary(projects, { observationsOf = {} } = {}) {
54
+ export async function fleetSummary(projects, { observationsOf = {}, statesOf = {} } = {}) {
47
55
  const rows = [];
48
- for (const { name, ledger } of projects) rows.push(await fleetProject(name, ledger, observationsOf[name] ?? null));
56
+ for (const { name, ledger } of projects) rows.push(await fleetProject(name, ledger, observationsOf[name] ?? null, statesOf[name] ?? null));
49
57
  const totals = rows.reduce((acc, row) => {
50
- for (const key of ['entries', 'wanted', 'expected', 'retired', 'earned', 'observed_entries', 'never_checked', 'implied_weekly_checks']) {
58
+ for (const key of ['entries', 'wanted', 'expected', 'retired', 'earned', 'observed_entries', 'never_checked', 'uncertain_entries', 'activity_entries', 'implied_weekly_checks']) {
51
59
  acc[key] = (acc[key] ?? 0) + row[key];
52
60
  }
53
61
  return acc;
54
62
  }, {});
55
63
  return {
56
- v: 1, projects: rows, totals,
64
+ v: 2, projects: rows, totals,
57
65
  // The sum answers "how big is the operation". It is not a merged view: the same platform
58
66
  // counted in two projects is two intents, and only a human can say which one they meant.
59
67
  intent_merged_across_repos: false,
@@ -75,6 +83,9 @@ export function renderFleet(summary, out = console.log) {
75
83
  + `${String(t.never_checked).padStart(13)} (sum of counts)`);
76
84
  out('');
77
85
  out(`implied weekly check volume across the fleet: ${t.implied_weekly_checks}`);
86
+ for (const row of summary.projects) {
87
+ out(`${row.project}: latest attempt ${row.last_attempt_at ?? 'not recorded'}; last conclusive check ${row.last_success_at ?? 'not recorded'}; last link verification ${row.last_link_verification_at ?? 'not recorded'}; activity ${row.activity_available ? `${row.activity_entries} entr(ies)` : 'not available'}; uncertain ${row.uncertain_entries}`);
88
+ }
78
89
  if (summary.projects.some(row => row.problems)) {
79
90
  out(`ledger problems: ${summary.projects.map(row => `${row.project}=${row.problems}`).filter(s => !s.endsWith('=0')).join(', ')}`);
80
91
  }
File without changes
package/cli/import.js CHANGED
@@ -54,6 +54,7 @@ export async function readImport(path, {
54
54
 
55
55
  const resolved = resolveMapping(table.header, { supplier, overrides: map, targetProvided: Boolean(target) });
56
56
  if (resolved.error) return { error: resolved.error, header: table.header };
57
+ const targetExpectation = supplier === 'google_search_console' && !resolved.mapping.target && Boolean(target);
57
58
 
58
59
  const unreadable = table.records.filter(record => record.error).map(record => ({ row: record.line, reason: record.error }));
59
60
  const readable = table.records.filter(record => record.values);
@@ -63,7 +64,12 @@ export async function readImport(path, {
63
64
  // means. Nothing is inferred — without --target the resolver above refuses the file.
64
65
  if (!resolved.mapping.target) {
65
66
  const targetUrl = targetKind === 'domain' ? `https://${target}/` : target;
66
- for (const row of rows) row.target_url = targetUrl;
67
+ for (const row of rows) {
68
+ row.target_url = targetUrl;
69
+ // This is the caller's target expectation, not a destination observed by Google.
70
+ // The verifier can use the saved run scope without asserting an exact root-page link.
71
+ if (targetExpectation) row.target_basis = 'user_supplied_target_expectation';
72
+ }
67
73
  }
68
74
 
69
75
  const targetUrl = targetKind === 'domain' ? `https://${target}/` : target;
File without changes
File without changes
@@ -0,0 +1,115 @@
1
+ // Where `agentlinkops connect` keeps the API key a person pastes, so cloud commands work in any
2
+ // folder without environment variables. Never inside a repository:
3
+ //
4
+ // macOS the login Keychain, through /usr/bin/security. `security -i` takes the key on stdin,
5
+ // so it never appears in a process listing.
6
+ // Linux ~/.agentlinkops/keys/, one file per cloud origin, mode 600 in a mode 700 directory
7
+ // owned by the person; anything looser is ignored rather than trusted.
8
+ // other nothing is saved; AGENTLINKOPS_API_KEY keeps working everywhere.
9
+ //
10
+ // One key per cloud origin. Reads are synchronous because cloudConnection() is.
11
+ import { execFile, execFileSync } from 'node:child_process';
12
+ import { createHash, randomBytes } from 'node:crypto';
13
+ import { lstatSync, readFileSync } from 'node:fs';
14
+ import * as fs from 'node:fs/promises';
15
+ import { homedir } from 'node:os';
16
+ import { isAbsolute, join } from 'node:path';
17
+ import { ConfigError } from './config.js';
18
+
19
+ export const API_KEY_PATTERN = /^lt_[0-9a-f]{64}$/;
20
+ // main() attaches the reader to the environment object it hands every command, under this
21
+ // symbol: it never reaches a child process, a JSON dump or a test that passes its own env.
22
+ export const STORED_KEY = Symbol.for('agentlinkops.storedKey');
23
+
24
+ const SECURITY = '/usr/bin/security';
25
+ const SERVICE = 'agentlinkops-cli';
26
+ // An origin is a Keychain account name and a file-name input: canonical https only, no spaces
27
+ // or quotes, so it can sit unquoted in a `security -i` command line.
28
+ const canonical = origin => {
29
+ try { const url = new URL(origin); return url.protocol === 'https:' && url.origin === origin && !/[\s"'\\]/.test(origin); }
30
+ catch { return false; }
31
+ };
32
+
33
+ const runSync = (file, args) => execFileSync(file, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000 });
34
+ const run = (file, args, input) => new Promise((resolve, reject) => {
35
+ const child = execFile(file, args, { encoding: 'utf8', timeout: 10000 }, (error, stdout) => error ? reject(error) : resolve(stdout));
36
+ child.stdin.end(input);
37
+ });
38
+
39
+ function keychainStore({ exec = runSync, execInput = run }) {
40
+ const store = {
41
+ where: 'your macOS Keychain',
42
+ readSync(origin) {
43
+ if (!canonical(origin)) return null;
44
+ try {
45
+ const key = exec(SECURITY, ['find-generic-password', '-s', SERVICE, '-a', origin, '-w']).trim();
46
+ return API_KEY_PATTERN.test(key) ? key : null;
47
+ } catch { return null; } // 44: no such item. Anything else reads as no saved key.
48
+ },
49
+ async save(origin, key) {
50
+ if (!canonical(origin) || !API_KEY_PATTERN.test(key)) throw new ConfigError('Only a complete API key for an https origin can be saved.');
51
+ await execInput(SECURITY, ['-i'], `add-generic-password -U -s ${SERVICE} -a ${origin} -w ${key}\n`).catch(() => {});
52
+ // `security -i` reports per-command failures on its own output, so read the key back.
53
+ if (store.readSync(origin) !== key) throw new ConfigError('The key could not be saved in your macOS Keychain. Set AGENTLINKOPS_API_KEY instead.');
54
+ },
55
+ async remove(origin) {
56
+ if (!canonical(origin)) return false;
57
+ try { exec(SECURITY, ['delete-generic-password', '-s', SERVICE, '-a', origin]); return true; } catch { return false; }
58
+ },
59
+ };
60
+ return store;
61
+ }
62
+
63
+ function fileStore({ home, uid = process.getuid?.() }) {
64
+ const dir = join(home, '.agentlinkops', 'keys');
65
+ const fileFor = origin => join(dir, `${createHash('sha256').update(origin).digest('hex').slice(0, 32)}.json`);
66
+ const own = info => uid !== undefined && info.uid === uid;
67
+ const privateDir = info => info.isDirectory() && own(info) && (info.mode & 0o777) === 0o700;
68
+ const privateFile = info => info.isFile() && info.nlink === 1 && own(info) && (info.mode & 0o777) === 0o600;
69
+ return {
70
+ where: '~/.agentlinkops/keys (readable only by you)',
71
+ readSync(origin) {
72
+ if (!canonical(origin)) return null;
73
+ try {
74
+ if (!privateDir(lstatSync(dir)) || !privateFile(lstatSync(fileFor(origin)))) return null;
75
+ const record = JSON.parse(readFileSync(fileFor(origin), 'utf8'));
76
+ return record?.origin === origin && API_KEY_PATTERN.test(record.key) ? record.key : null;
77
+ } catch { return null; }
78
+ },
79
+ async save(origin, key) {
80
+ if (!canonical(origin) || !API_KEY_PATTERN.test(key)) throw new ConfigError('Only a complete API key for an https origin can be saved.');
81
+ for (const path of [join(home, '.agentlinkops'), dir]) {
82
+ try { await fs.mkdir(path, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
83
+ }
84
+ if (!privateDir(await fs.lstat(dir))) throw new ConfigError(`${dir} must be a directory only you can open (mode 700). Fix it, or set AGENTLINKOPS_API_KEY instead.`);
85
+ const path = fileFor(origin), temporary = `${path}.${randomBytes(8).toString('hex')}.tmp`;
86
+ try {
87
+ await fs.writeFile(temporary, `${JSON.stringify({ origin, key })}\n`, { flag: 'wx', mode: 0o600 });
88
+ await fs.rename(temporary, path);
89
+ } finally { await fs.unlink(temporary).catch(() => {}); }
90
+ },
91
+ async remove(origin) {
92
+ if (!canonical(origin)) return false;
93
+ try { await fs.unlink(fileFor(origin)); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; }
94
+ },
95
+ };
96
+ }
97
+
98
+ const unsupported = {
99
+ where: null,
100
+ readSync: () => null,
101
+ async save() { throw new ConfigError('This platform has no supported place to save a key yet. Set AGENTLINKOPS_API_KEY instead.'); },
102
+ async remove() { return false; },
103
+ };
104
+
105
+ export function createKeyStore({ platform = process.platform, home = homedir(), exec, execInput, uid } = {}) {
106
+ if (platform === 'darwin') return keychainStore({ exec, execInput });
107
+ if (platform === 'linux' && typeof home === 'string' && isAbsolute(home)) return fileStore({ home, uid });
108
+ return unsupported;
109
+ }
110
+
111
+ /** The environment a command sees, plus a reader for keys saved by `agentlinkops connect`. */
112
+ export function withStoredKeys(env, store) {
113
+ if (!store) return env;
114
+ return { ...env, [STORED_KEY]: origin => { const key = store.readSync(origin); return key ? { key, where: store.where } : null; } };
115
+ }
package/cli/ledger.js CHANGED
@@ -214,6 +214,7 @@ export function toWatchInput(entry) {
214
214
  // caller can say so rather than discovering it when a reference comes back short.
215
215
  const reference = entry.ref ? `${entry.id} ${entry.ref}` : entry.id;
216
216
  return {
217
+ intent: ({wanted:'prospect',expected:'earned',retired:'retired'})[entry.intent],
217
218
  sourceUrl: entry.source, targetUrl: entry.target, targetScope: entry.scope,
218
219
  cadenceSeconds: cadenceSeconds(entry.cadence ?? DEFAULT_CADENCE[entry.intent] ?? 'daily'),
219
220
  expectedAnchor: entry.expect?.anchor ?? null,
package/cli/lifecycle.js CHANGED
File without changes
package/cli/linktrail.mjs CHANGED
File without changes
@@ -15,11 +15,26 @@ export const LOCAL_RESULT_SCHEMA = 'agentlinkops.local-check.v1';
15
15
 
16
16
  const timestamp = z.string().refine(value => Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value);
17
17
  const strings = z.array(z.string());
18
+ const digest = z.string().regex(/^[a-f0-9]{64}$/u);
19
+ const capturePin = z.string().min(1).max(128).regex(/^[^\x00-\x20\x7f]+$/u);
20
+ const bodyDiagnosticSchema = z.strictObject({ version: z.literal(1), bytes: z.number().int().min(0).max(64 * 1024),
21
+ complete: z.boolean(), truncated: z.boolean(), sha256: digest, looksLikeHtml: z.boolean() });
22
+ const googlebotSchema = z.strictObject({ productToken: z.literal('Googlebot'), allowed: z.boolean().nullable(),
23
+ reason: z.string().nullable(), matchedRule: z.string().nullable(), crawlDelaySeconds: z.number().finite().min(0).nullable() });
24
+ const captureSchema = z.strictObject({ version: z.literal(1), representation: z.literal('raw_html'),
25
+ requestedUrl: z.string(), finalUrl: z.string(), httpStatus: z.number().int().min(100).max(599), acquiredAt: timestamp,
26
+ body: z.strictObject({ complete: z.literal(true), truncated: z.literal(false), bytes: z.number().int().min(0).max(2 * 1024 * 1024),
27
+ sha256: digest, decodedSha256: digest, charset: z.string().max(64).nullable().optional() }),
28
+ transport: z.strictObject({ id: capturePin, version: capturePin, qualified: z.literal(true) }),
29
+ coverage: z.strictObject({ mainDocument: z.literal(true), iframes: z.literal(false), shadowRoots: z.literal(false) }) });
18
30
  const redirectSchema = z.strictObject({ from: z.string(), to: z.string(), status: z.number().int() });
19
31
  const robotsSchema = z.strictObject({ allowed: z.boolean().nullable(), reason: z.string().nullable(),
20
32
  matchedRule: z.string().nullable(), crawlDelaySeconds: z.number().nullable(), fetched: z.string().nullable(),
21
33
  sourceUrl: z.string().optional(), robotsUrl: z.string().optional(), httpStatus: z.number().int().nullable().optional(),
22
- productToken: z.string().optional(), redirects: z.array(redirectSchema).max(5).optional(), retryAfterSeconds: z.number().optional() });
34
+ productToken: z.string().optional(), redirects: z.array(redirectSchema).max(5).optional(), retryAfterSeconds: z.number().finite().min(0).max(86400).optional(),
35
+ googlebot: googlebotSchema.optional(), decisionReason: z.string().nullable().optional(),
36
+ policy: z.literal('customer_supplied_monitor').optional(), fetchAuthorized: z.boolean().optional(), overrideUsed: z.literal(false).optional(),
37
+ bodyDiagnostic: bodyDiagnosticSchema.optional() });
23
38
  const readinessSchema = z.strictObject({ requiresRender: z.boolean(), reason: z.string().nullable(), possibleLoginWall: z.boolean(),
24
39
  hasVisiblePassword: z.boolean().optional(), targetInScript: z.boolean().optional(), emptyAppRoot: z.boolean().optional(), emptyProfileRoot: z.boolean().optional() });
25
40
  const occurrenceSchema = z.strictObject({ href: z.string(), targetUrl: z.string(), anchor: z.string(), rel: strings,
@@ -28,15 +43,18 @@ const observationSchema = z.strictObject({
28
43
  state: z.enum(['present','absent','unknown','source_unavailable']), reason: z.string(), sourceUrl: z.string(), finalUrl: z.string(), targetUrl: z.string(),
29
44
  targetScope: z.enum(['exact','domain','subdomain','path']), httpStatus: z.number().int().nullable(), checkedAt: timestamp,
30
45
  occurrences: z.array(occurrenceSchema).max(1000),
31
- directives: z.strictObject({ meta: z.array(z.strictObject({ name: z.string(), content: z.string() })), xRobotsTag: strings, noindex: z.boolean(), nofollow: z.boolean(), indexingStatus: z.string() }),
46
+ directives: z.strictObject({ meta: z.array(z.strictObject({ name: z.string(), content: z.string() })), xRobotsTag: strings, noindex: z.boolean(), nofollow: z.boolean(), indexingStatus: z.string(),
47
+ googlebot: z.strictObject({ noindex: z.boolean(), nofollow: z.boolean() }).optional() }),
32
48
  redirects: z.array(redirectSchema),
33
49
  evidence: z.strictObject({ method: z.string(), checkerVersion: z.string(), complete: z.boolean(), fetchedAt: timestamp.nullable(),
34
50
  bytes: z.number().optional(), contentType: z.string().nullable().optional(), etag: z.string().nullable().optional(), lastModified: z.string().nullable().optional(),
35
- baseUrl: z.string().optional(), rendered: z.boolean().optional(), sha256: z.string().optional(), parseErrors: strings.optional(),
36
- readiness: readinessSchema.optional(), renderEligibility: z.enum(['required', 'comparison']).optional() }),
51
+ baseUrl: z.string().optional(), rendered: z.literal(false).optional(), sha256: digest.optional(), decodedSha256: digest.optional(),
52
+ capture: captureSchema.nullable().optional(), parseErrors: strings.optional(),
53
+ readiness: readinessSchema.optional(), renderEligibility: z.enum(['required', 'comparison']).optional(),
54
+ renderAttempt: z.strictObject({ attempted: z.literal(false), reason: z.literal('render_not_configured') }).optional() }),
37
55
  robots: robotsSchema.optional(), robotsHistory: z.array(robotsSchema).max(6).optional(),
38
56
  sourceResponse: z.strictObject({ url: z.string(), httpStatus: z.number().int(), contentType: z.string().max(128).nullable(),
39
- retryAfterSeconds: z.number().nullable(), challenge: z.boolean() }).optional(),
57
+ retryAfterSeconds: z.number().nullable(), challenge: z.boolean(), bodyDiagnostic: bodyDiagnosticSchema.optional() }).optional(),
40
58
  expectations: z.strictObject({ expectedAnchor: z.string().nullable(), expectedRel: strings.nullable(), anchorMatches: z.boolean().nullable(), relMatches: z.boolean().nullable(), satisfied: z.boolean() }).optional(),
41
59
  linkSignature: z.string().optional(), warnings: strings.optional(), retryAfterSeconds: z.number().optional(),
42
60
  });
@@ -88,11 +106,28 @@ export function validateLocalResult(result) {
88
106
  const rejectBytes = value => {
89
107
  if (!value || typeof value !== 'object') return;
90
108
  for (const [key, child] of Object.entries(value)) {
91
- if (['html','body','headers'].includes(key.toLowerCase())) throw new Error('portable observations cannot contain raw HTML/body/headers');
109
+ if (['html','body','headers'].includes(key.toLowerCase())
110
+ && !(key === 'body' && value === observation.evidence?.capture)) throw new Error('portable observations cannot contain raw HTML/body/headers');
92
111
  rejectBytes(child);
93
112
  }
94
113
  };
95
114
  rejectBytes(observation);
115
+ const capture = observation.evidence.capture;
116
+ if (capture) {
117
+ if (capture.requestedUrl !== input.source || capture.finalUrl !== observation.finalUrl
118
+ || !validatePublicUrl(capture.finalUrl).valid || capture.httpStatus !== observation.httpStatus
119
+ || capture.acquiredAt !== observation.evidence.fetchedAt
120
+ || capture.body.bytes !== observation.evidence.bytes || capture.body.sha256 !== observation.evidence.sha256
121
+ || capture.body.decodedSha256 !== observation.evidence.decodedSha256
122
+ || observation.evidence.method !== 'http_html') throw new Error('portable capture does not match its observation');
123
+ if (capture.body.charset != null) {
124
+ try { new TextDecoder(capture.body.charset, { fatal: true }); }
125
+ catch { throw new Error('invalid portable capture charset'); }
126
+ }
127
+ }
128
+ if (observation.evidence.renderAttempt && (observation.state !== 'unknown' || observation.evidence.complete !== false
129
+ || !['possible_render_required', 'navigation_requires_browser'].includes(observation.reason)
130
+ || observation.evidence.renderEligibility !== 'required')) throw new Error('invalid portable refused-render observation');
96
131
  if (observation.occurrences.some(item => !item || typeof item.targetUrl !== 'string' || !Array.isArray(item.rel)
97
132
  || !matchesTarget(item.targetUrl, input.target, input.scope))) throw new Error('invalid portable occurrence');
98
133
  if (observation.state === 'present' && (!observation.evidence.complete || !observation.occurrences.length)) throw new Error('present requires complete matching occurrences');
package/cli/main.js CHANGED
@@ -1,5 +1,8 @@
1
+ import {projectPlacements} from './placement-projection.js';
2
+ import {chronologyMain} from './chronology.js';
1
3
  import { acquireSyncLock } from './sync-lock.js';
2
4
  import {lifecycleMain} from './lifecycle.js';
5
+ import {campaignMain,CAMPAIGN_CLI_HELP} from './campaign-bridge.js';
3
6
  import {admissionMain} from './admission.js';
4
7
  import { commandsMain } from './commands.js';
5
8
  import { disavowMain } from './disavow.js';
@@ -21,8 +24,10 @@ import { checkToFile, adoptLocalResult } from './local-result.js';
21
24
  import { disagreements, transitions } from './status.js';
22
25
  import { readImport, parseMap } from './import.js';
23
26
  import { freezeDataset, renderReport, datasetDigest } from './report.js';
24
- import { createClient, CloudError } from './client.js';
25
- import { cloudConnection, connectMain, keySetup } from './connection.js';
27
+ import { createClient, CloudError, CLI_VERSION } from './client.js';
28
+ import { cloudConnection, connectKey, connectMain, keySetup } from './connection.js';
29
+ import { createKeyStore, withStoredKeys } from './key-store.js';
30
+ import { askSecret } from './prompt.js';
26
31
  import { syncPlan, pushExpectations, pullLifecycleMirrors, pullEvents, cloudObservationRows, watchIndex, pullTargetEvents } from './sync.js';
27
32
  import { SUPPLIER_NAMES } from './adapters/suppliers.js';
28
33
  import { fleetSummary, renderFleet } from './fleet.js';
@@ -44,6 +49,8 @@ import { citationMain } from './citation.js';
44
49
 
45
50
  const USAGE = `agentlinkops — a backlink ledger that lives in your repository
46
51
 
52
+ agentlinkops --version print the installed CLI version
53
+
47
54
  agentlinkops skill [--url|--list [--json]|NAME] read the agent reference or a named skill
48
55
  before the first AgentLinkOps call
49
56
  agentlinkops init create .agentlinkops/ here
@@ -52,9 +59,12 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
52
59
  [--mode local|hosted|external] read-only setup plan; no account required
53
60
  agentlinkops add --source URL --target URL [--intent wanted|expected] [--scope …]
54
61
  [--anchor TEXT] [--rel a,b] [--ref TEXT] [--tag t --tag t] [--note TEXT]
62
+ agentlinkops chronology pull --watch-id ID [--limit N] [--cursor TOKEN] [--all] [--out FILE]
63
+ agentlinkops chronology export --pages FILE [--submissions FILE] [--out FILE]
55
64
  agentlinkops index pull --watch-id ID [--limit N] [--before ID]
65
+ agentlinkops campaign plan|apply|reconcile|rollback|pull-plan|pull-apply|pull-rollback|outcomes-plan|outcomes-apply
66
+ preview and reconcile portable campaign records; no sending or automatic checks
56
67
  agentlinkops lifecycle get|update|clear|renew|report
57
- agentlinkops admission list|add|update|remove|evaluate|reevaluate|receipt --project ID
58
68
  agentlinkops disavow list|history|propose|import|export|approve|reject|delete
59
69
  agentlinkops index import FILE | export [--out FILE] | csv URLS.csv [--out FILE]
60
70
  agentlinkops index submissions-import FILE | submissions-export --project-id ID [--out FILE]
@@ -63,7 +73,7 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
63
73
  agentlinkops import FILE --target DOMAIN [--from SUPPLIER] [--map source=COL,target=COL]
64
74
  [--exact-url] [--no-subdomains] [--generated-at ISO] [--json]
65
75
  agentlinkops adopt CRM.sqlite [--scope exact|domain] [--write]
66
- agentlinkops fleet --project NAME=LEDGER [--project …] [--observations-of NAME=FILE] [--json]
76
+ agentlinkops fleet --project NAME=LEDGER [--project …] [--observations-of NAME=FILE] [--state-of NAME=FILE] [--json]
67
77
  agentlinkops platforms [HOST] [--observations FILE]… [--benchmark FILE]… [--seed FILE]…
68
78
  [--for-candidates FILE] [--json]
69
79
  agentlinkops context COMMAND… first-party search context and site profile
@@ -78,7 +88,9 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
78
88
  .agentlinkops/citations/; live engines need
79
89
  engine-specific setup; the mock engine makes no paid calls
80
90
  agentlinkops receive --body FILE --headers FILE signed notification; authenticated pull
81
- agentlinkops connect --workspace ID --project-id ID [--origin URL] [--selection FILE] [--oauth --issuer URL --client-id ID [--credential-ref REF]]
91
+ agentlinkops connect [--project-id ID] [--workspace ID] [--origin URL] [--selection FILE] [--oauth --issuer URL --client-id ID [--credential-ref REF]]
92
+ paste an API key once; it is saved outside the
93
+ repository and the key names its workspace
82
94
  agentlinkops tools [TOOLSET] [--json] [--refresh] cloud commands by toolset (offline snapshot;
83
95
  --refresh reads the live catalog)
84
96
  agentlinkops describe NAME [--examples] [--output-schema] [--json] one command's exact schema
@@ -87,7 +99,8 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
87
99
  agentlinkops agent setup [--client ID ...] [--scope project|user] [--origin URL] [--dry-run]
88
100
  core connection/reference and the right MCP view;
89
101
  optional --skill ID ... / --recipe ID ... / --all-skills;
90
- stores no credential; preserves customer instructions
102
+ stores no credential unless you paste a CLI key
103
+ when asked; preserves customer instructions
91
104
  agentlinkops agent status what is installed where
92
105
  agentlinkops agent remove [--skill ID ...] [--recipe ID ...] [--scope project|user] [--apply] [--json]
93
106
  agentlinkops agent recover [--scope project|user] [--apply] [--json]
@@ -100,7 +113,7 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
100
113
  agentlinkops adopt-result FILE [--intent wanted|expected] save result to local ledger
101
114
  agentlinkops receipt add --file CLAIM.json project.site declares public site hosts
102
115
  agentlinkops receipt history [--performance FILE.json] claims, history and optional dated context
103
- agentlinkops status [--json]
116
+ agentlinkops status [--all] [--json]
104
117
  agentlinkops diff [--json]
105
118
  agentlinkops report [--out FILE] [--title T] [--brand B] [--as-of ISO]
106
119
  [--include-notes] [--include-retired] [--digest-only] [--json]
@@ -122,7 +135,25 @@ evidence · 2 usage, configuration or ledger error. Unknown is nonfailing by def
122
135
  Names: \`linktrail\` still runs this CLI, LINKTRAIL_* variables are still read, and an existing
123
136
  .linktrail/ directory is still used, each with a one-line notice, until the DP-0029 cutover.`;
124
137
 
125
- const parseArgs = argv => parseArguments(argv,["tag", "project", "observations-of", "observations", "benchmark", "seed"],{tag:[]});
138
+ // After `agent setup` on a terminal: the CLI half, so the folder's agent can also run
139
+ // `agentlinkops call`. Optional (Enter skips), and setup has already succeeded either way.
140
+ async function connectCliAfterSetup(origin, { env, ask, keyStore, fetchImpl, out }) {
141
+ let connection = null;
142
+ try { connection = cloudConnection({ cloud: { origin } }, env); } catch { /* Disagreeing variables: connect reports them. */ }
143
+ out('');
144
+ if (connection?.token) { out(`The agentlinkops CLI can use this account too (key in ${connection.tokenSource}).`); return; }
145
+ try {
146
+ const key = await connectKey({ origin, ask, keyStore, fetchImpl, out,
147
+ prompt: `To use the agentlinkops CLI as well, paste an API key from ${origin}/#/access (Create API key), or press Enter to skip: ` });
148
+ out(key
149
+ ? `The CLI is connected to ${key.account.workspace.name || 'your workspace'}. Try: agentlinkops call list_tasks`
150
+ : 'Skipped. Run `npx -y agentlinkops connect` when you want the CLI.');
151
+ } catch (error) {
152
+ out(`The CLI wasn't connected: ${error.serverMessage ?? error.message}. Run \`npx -y agentlinkops connect\` to try again.`);
153
+ }
154
+ }
155
+
156
+ const parseArgs = argv => parseArguments(argv,["tag", "project", "observations-of", "state-of", "observations", "benchmark", "seed"],{tag:[]});
126
157
 
127
158
  const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
128
159
 
@@ -135,7 +166,16 @@ function reportProblems(problems, what, out) {
135
166
  if (problems.length > 20) out(` … and ${problems.length - 20} more`);
136
167
  }
137
168
 
138
- export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), out = console.log, err = console.error, env = process.env, fetchImpl = globalThis.fetch, afterHistoryPersisted, afterStatePersisted } = {}) {
169
+ export async function main(argv = process.argv.slice(2), options) {
170
+ // The executables call main() with no options, the one caller that reads the person's saved
171
+ // key and asks on their terminal. Every other caller passes options and gets neither.
172
+ const executable = options === undefined;
173
+ const { cwd = process.cwd(), out = console.log, err = console.error, fetchImpl = globalThis.fetch, localFetchImpl = null, afterHistoryPersisted, afterStatePersisted, signal,
174
+ keyStore = executable ? createKeyStore() : null,
175
+ ask = executable && process.stdin.isTTY === true && process.stderr.isTTY === true ? askSecret : null } = options ?? {};
176
+ if (argv.length === 1 && argv[0] === '--version') { out(CLI_VERSION); return 0; }
177
+ if (argv[0] === 'campaign' && argv.length===2 && ['--help','help'].includes(argv[1])) { out(CAMPAIGN_CLI_HELP); return 0; }
178
+ const env = withStoredKeys(options?.env ?? process.env, keyStore);
139
179
  if (argv[0] === 'gsc-links') return await gscLinksMain(argv, { cwd, out });
140
180
  // Citation watches keep their own files under .agentlinkops/citations/ and never
141
181
  // touch the link ledger, so they dispatch before its requirement like context does.
@@ -151,24 +191,27 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
151
191
  try {
152
192
  if (args['recover-cursors'] !== undefined && (command !== 'sync' || args['recover-cursors'] !== true || args['pull-only'] !== true || args['push-only']))
153
193
  throw new ConfigError('Cursor recovery requires agentlinkops sync --pull-only --recover-cursors; it performs no remote writes.');
194
+ if (command === 'campaign') return await campaignMain(args,{cwd,env,fetchImpl,out});
154
195
  if (command === 'lifecycle') return await lifecycleMain(args,{cwd,env,fetchImpl,out,err});
155
196
  if (command === 'admission') { const config=await loadConfig({cwd}); return await admissionMain(argv.slice(1),{client:createClient({...cloudConnection(config,env),fetchImpl}),out,err}); }
156
197
  if (command === 'disavow') return await disavowMain(args,{cwd,env,fetchImpl,out});
198
+ if (command === 'chronology') return await chronologyMain(args,{cwd,env,fetchImpl,out});
157
199
  if (command === 'index') return await indexMain(args,{cwd,env,fetchImpl,out});
158
200
  if (command === 'setup') return await setupPlanMain(argv, { cwd, out });
159
201
  if (command === 'tools' || command === 'describe' || command === 'schema' || command === 'call') return await commandsMain(argv, { cwd, out, err, env, fetchImpl });
160
202
  if (command === 'skill') return await skillMain(argv, { out });
161
- if (command === 'agent') return await agentMain(argv, { cwd, out, err });
203
+ if (command === 'agent') return await agentMain(argv, { cwd, out, err, env, connectCli: ask && keyStore ? origin => connectCliAfterSetup(origin, { env, ask, keyStore, fetchImpl, out }) : null });
162
204
  if (command === 'connect') {
163
- if (args._.length !== 1 || args.tag.length || Object.keys(args).some(key => !['_', 'tag', 'origin', 'workspace', 'project-id', 'selection', 'oauth', 'issuer', 'client-id', 'credential-ref'].includes(key))) throw new ConfigError('connect --workspace ID --project-id ID [--origin URL] [--selection FILE] [--oauth --issuer URL --client-id ID [--credential-ref REF]]');
164
- return await connectMain(args, { cwd, out, env, fetchImpl });
205
+ if (args._.length !== 1 || args.tag.length || Object.keys(args).some(key => !['_', 'tag', 'origin', 'workspace', 'project-id', 'selection', 'oauth', 'issuer', 'client-id', 'credential-ref'].includes(key))) throw new ConfigError('connect [--project-id ID] [--workspace ID] [--origin URL] [--selection FILE] [--oauth --issuer URL --client-id ID [--credential-ref REF]]');
206
+ return await connectMain(args, { cwd, out, env, fetchImpl, keyStore, ask });
165
207
  }
166
208
  if (command === 'check' && (args.source !== undefined || args.target !== undefined)) {
167
209
  if (typeof args.source !== 'string' || typeof args.target !== 'string') throw new ConfigError('check requires both --source URL and --target URL');
168
210
  const allowed = new Set(['_', 'source', 'target', 'scope', 'json', 'out']);
169
211
  if (Object.keys(args).some(key => key !== 'tag' && !allowed.has(key)) || args.tag.length || args._.length !== 1) throw new ConfigError('unsupported option for a single local check');
170
212
  if (args.out !== undefined && (typeof args.out !== 'string' || !args.out.trim())) throw new ConfigError('--out requires a file path');
171
- const result = await checkToFile({ source: args.source, target: args.target, scope: args.scope }, args.out ? resolve(cwd, args.out) : null);
213
+ const result = await checkToFile({ source: args.source, target: args.target, scope: args.scope }, args.out ? resolve(cwd, args.out) : null,
214
+ { fetchImpl: localFetchImpl });
172
215
  out(args.json ? JSON.stringify(result, null, 2) : `${result.observation.state} ${result.observation.reason} ${result.checked_at}${args.out ? ` saved ${args.out}` : ''}`);
173
216
  return 0;
174
217
  }
@@ -306,7 +349,15 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
306
349
  if (!name || !path) throw new ConfigError(`--observations-of expects NAME=FILE, got "${value}"`);
307
350
  observationsOf[name] = path;
308
351
  }
309
- const summary = await fleetSummary(pairs, { observationsOf });
352
+ const statesOf = {};
353
+ for (const value of [].concat(args['state-of'] ?? [])) {
354
+ const separator = String(value).indexOf('=');
355
+ const name = String(value).slice(0, separator), path = String(value).slice(separator + 1);
356
+ if (separator < 1 || !path || !pairs.some(project => project.name === name)) throw new ConfigError('--state-of expects a declared NAME=FILE');
357
+ if (Object.hasOwn(statesOf, name)) throw new ConfigError(`Duplicate state input for ${name}`);
358
+ statesOf[name] = path;
359
+ }
360
+ const summary = await fleetSummary(pairs, { observationsOf, statesOf });
310
361
  if (args.json) { out(JSON.stringify(summary, null, 1)); return 0; }
311
362
  return renderFleet(summary, out);
312
363
  }
@@ -320,7 +371,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
320
371
  // DP-0017's context surface owns its config and files and needs no ledger, so it runs
321
372
  // before the ledger check below. contextMain parses its own argv; hand it everything
322
373
  // after the command word (found positionally, because flags may precede it).
323
- return await contextMain(argv.slice(argv.indexOf(command) + 1), { cwd, out, err });
374
+ return await contextMain(argv.slice(argv.indexOf(command) + 1), { cwd, out, err,signal });
324
375
  }
325
376
  if (command === 'doctor') {
326
377
  // Doctor must run exactly where the ledger cannot be trusted yet: its job is to name
@@ -328,7 +379,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
328
379
  // file itself. Takes no flags and repairs nothing. Awaited on purpose: a promise returned
329
380
  // from inside the try escapes the ConfigError handler below, and a disagreeing pair of
330
381
  // token spellings would surface as a stack trace instead of the exit-2 message.
331
- return await doctorMain(argv.slice(argv.indexOf(command) + 1), { cwd, out, err });
382
+ return await doctorMain(argv.slice(argv.indexOf(command) + 1), { cwd, out, err, env });
332
383
  }
333
384
 
334
385
  const config = await loadConfig({ cwd });
@@ -392,6 +443,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
392
443
  // so it is unchanged by a cosmetic edit to the template and changes the moment a figure does.
393
444
  const asOf = args['as-of'] === true ? null : (args['as-of'] ?? null);
394
445
  const dataset = freezeDataset(ledger.entries, observations.rows, {
446
+ activityState: await readState(config.paths.state),
395
447
  asOf: asOf ?? new Date().toISOString().slice(0, 10),
396
448
  title: args.title === true ? 'Backlink report' : (args.title ?? 'Backlink report'),
397
449
  includeNotes: args['include-notes'] === true,
@@ -500,24 +552,29 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
500
552
  }
501
553
 
502
554
  if (command === 'status' || command === 'diff') {
503
- const found = command === 'status' ? disagreements(ledger.entries, observations.rows) : transitions(observations.rows);
504
- if (args.json) { out(JSON.stringify(found, null, 1)); return 0; }
555
+ const activityState = command === 'status' ? await readState(config.paths.state) : null;
556
+ const found = command === 'status' ? disagreements(ledger.entries, observations.rows, { state: activityState }) : transitions(observations.rows);
557
+ const activity = command === 'status' && args.all ? projectPlacements(ledger.entries, observations.rows, { state: activityState }).map(({ entry, row, ...facts }) => ({ id: entry.id, source: entry.source, target: entry.target, scope: entry.scope, ...facts })) : null;
558
+ if (args.json) { out(JSON.stringify(activity ? { disagreements: found, placements: activity } : found, null, 1)); return 0; }
505
559
  reportProblems(ledger.problems, 'ledger line(s)', out);
506
560
  if (command === 'diff') {
507
561
  if (!found.length) out('no state changes recorded');
508
562
  for (const change of found.slice(0, 100)) out(` ${change.at.slice(0, 10)} ${change.from} -> ${change.to} ${change.id}`);
509
563
  return 0;
510
564
  }
511
- const groups = { appeared: 'appeared', lost: 'LOST', suspected_missing: 'suspected missing', cannot_say: 'cannot say', never_checked: 'never checked' };
565
+ const groups = { appeared: 'appeared', lost: 'confirmed missing', suspected_missing: 'suspected missing', cannot_say: 'cannot say', never_checked: 'never checked' };
512
566
  for (const [kind, label] of Object.entries(groups)) {
513
567
  const rows = found.filter(item => item.kind === kind);
514
568
  if (!rows.length) continue;
515
569
  out(`\n${label} (${rows.length})`);
516
570
  for (const item of rows.slice(0, 50)) {
517
571
  out(` ${item.entry.id} ${item.entry.source}${item.row ? ` [${item.row.reason}]` : ''}`);
572
+ out(` Latest attempt: ${item.activity?.latest_attempt?.checked_at ?? 'not retained'} (${item.activity?.latest_attempt?.reason ?? 'unknown'})`);
573
+ out(` Last conclusive: ${item.activity?.last_successful_observation?.checked_at ?? 'not retained'}; last link verification: ${item.activity?.last_link_verification?.checked_at ?? 'not retained'}`);
518
574
  }
519
575
  }
520
576
  if (!found.length) out('intent and observations agree on every entry');
577
+ if (activity) for (const item of activity) out(` ${item.id}: latest attempt ${item.latest_attempt?.checked_at ?? 'not retained'}; last conclusive ${item.last_successful_observation?.checked_at ?? 'not retained'}; last link verification ${item.last_link_verification?.checked_at ?? 'not retained'}`);
521
578
  return 0;
522
579
  }
523
580
 
@@ -590,13 +647,14 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
590
647
  if (ledger.problems.length) { reportProblems(ledger.problems, 'ledger line(s)', err); return 2; }
591
648
  const state = await readState(config.paths.state);
592
649
  const selected = selectEntries(ledger.entries, { filter: args.filter === true ? null : args.filter });
593
- const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state));
650
+ const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state, selected));
594
651
  if (!entries.length) { (args.json ? err : out)(`nothing due (${plural(selected.length, 'entry', 'entries')} in scope; pass --all to recheck)`); return 0; }
595
652
  if (!args.json) err(`checking ${plural(entries.length, 'entry', 'entries')}…`);
596
653
  const rows = await runCheck(entries, {
597
654
  concurrency: Number(args.concurrency ?? config.defaults.concurrency ?? DEFAULTS.concurrency),
598
655
  timeoutMs: Number(args.timeout ?? config.defaults.timeoutMs ?? DEFAULTS.timeoutMs),
599
656
  hostDelayMs: Number(args['host-delay'] ?? config.defaults.hostDelayMs ?? DEFAULTS.hostDelayMs),
657
+ fetchImpl: localFetchImpl,
600
658
  });
601
659
  // The mirror records changes; `state.json` records activity. Appending a row that says
602
660
  // the same thing as the one above it is 2 KB of git churn per link per run, and the only
@@ -625,6 +683,13 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
625
683
  err(JSON.stringify({error: {code: error.code, status: error.status, next: 'Keep this project ledger and its history. Resolve authorized read access for the configured project, then retry agentlinkops sync --pull-only --recover-cursors. Do not broaden grants or replay remote writes.'}}));
626
684
  return 2;
627
685
  }
686
+ if (command === 'sync' && error instanceof CloudError && error.status === 0 &&
687
+ ['CLOUD_TIMEOUT', 'CLOUD_NETWORK_ERROR'].includes(error.code)) {
688
+ // A local transport failure cannot establish whether a write committed.
689
+ // Print trusted recovery guidance, never attached exception/server text.
690
+ err(`agentlinkops: ${error.code}\nThe request outcome is unknown. Check the operation result before sending it again. Keep this project ledger and its history. Inspect accessible watches and their local references with agentlinkops sync --pull-only --recover-cursors. Do not broaden grants or replay remote writes.`);
691
+ return 2;
692
+ }
628
693
  err(`agentlinkops: ${error?.message ?? error}`);
629
694
  return 2;
630
695
  } finally {
package/cli/mcp-client.js CHANGED
@@ -62,7 +62,7 @@ function retryDelay(error, attempt, now) {
62
62
 
63
63
  async function sdkClient({url, token, workspaceId, fetchImpl}) {
64
64
  const {Client, StreamableHTTPClientTransport} = await import('@modelcontextprotocol/client');
65
- const client = new Client({name: 'agentlinkops-cli', version: '0.6.11'});
65
+ const client = new Client({name: 'agentlinkops-cli', version: '0.6.16'});
66
66
  const destination = new URL(url);
67
67
  const transport = new StreamableHTTPClientTransport(destination, {
68
68
  authProvider: {token: async () => token},
@@ -71,7 +71,7 @@ async function sdkClient({url, token, workspaceId, fetchImpl}) {
71
71
  if (target.href !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH');
72
72
  const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
73
73
  if (workspaceId) headers.set('X-Workspace-ID', workspaceId);
74
- headers.set('User-Agent', 'agentlinkops-cli/0.6.11');
74
+ headers.set('User-Agent', 'agentlinkops-cli/0.6.16');
75
75
  const response = await fetchImpl(input, {...init, headers, redirect: 'error', signal: AbortSignal.timeout(30000)});
76
76
  if (response.redirected || response.status >= 300 && response.status < 400
77
77
  || response.url && response.url !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH');