Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "seo",
"displayName": "seo",
"description": "Local-first SEO and AI-search diagnostics. Bundles the seo MCP server plus one SEO skill that gives an agent 50+ audit and report tools without filling its context window.",
"version": "0.2.37",
"version": "0.2.38",
"author": {
"name": "Ian Nuttall"
},
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "seo",
"version": "0.2.37",
"version": "0.2.38",
"description": "The SEO command for AI agents. Audit sites and research search opportunities with local, evidence-backed reports.",
"type": "module",
"license": "Apache-2.0",
Expand Down
58 changes: 57 additions & 1 deletion packages/cli/src/commands/workflows/diagnose-property.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { reportFollowups } from './diagnose-property.js'
import { agentActionsView } from '@seo/core'
import {
reportActionsWithTechnicalEvidence,
reportFollowups,
} from './diagnose-property.js'

function reportFixture(): Parameters<typeof reportFollowups>[0] {
return {
Expand Down Expand Up @@ -87,3 +91,55 @@ test('technical-only report asks for a crawl before a page follow-up', () => {
],
)
})

test('report actions include technical crawl fixes and reviews', () => {
const searchActions = [{ id: 'search:quick-win' }]
const technicalActions = [
{
id: 'crawl:title_duplicate',
kind: 'fix',
title: 'Duplicate titles',
action: 'Write a distinct title for each affected page.',
},
{
id: 'crawl:noindex',
kind: 'review',
title: 'Noindex pages',
action: 'Confirm that each noindex directive is intentional.',
},
]

assert.deepEqual(
reportActionsWithTechnicalEvidence(searchActions, technicalActions),
[...searchActions, ...technicalActions],
)
assert.deepEqual(
reportActionsWithTechnicalEvidence([], technicalActions),
technicalActions,
)

const view = agentActionsView(
{ actions: reportActionsWithTechnicalEvidence([], technicalActions) },
{ preferRootActions: true },
)
const findings = view.findings as {
counts: {
total: number
returned: number
fixes: number
reviews: number
open: number
}
completion: { state: string }
sourcePaths: string[]
}
assert.deepEqual(findings.counts, {
total: 2,
returned: 2,
fixes: 1,
reviews: 1,
open: 2,
})
assert.equal(findings.completion.state, 'pending')
assert.deepEqual(findings.sourcePaths, ['actions'])
})
22 changes: 16 additions & 6 deletions packages/cli/src/commands/workflows/diagnose-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ function compactTechnicalSection(section: ReturnType<typeof technicalSection>) {
}
}

export function reportActionsWithTechnicalEvidence<T, U>(
reportActions: readonly T[],
technicalActions?: readonly U[],
): Array<T | U> {
return technicalActions
? [...reportActions, ...technicalActions]
: [...reportActions]
}

function printTechnicalSection(
section: ReturnType<typeof technicalSection>,
options: { providerFree?: boolean } = {},
Expand Down Expand Up @@ -853,7 +862,7 @@ function workflowCommandMeta(input: {
overlap: topicOverlap,
})
: undefined
const urlModeActions = technicalBaseline?.report
const technicalActions = technicalBaseline?.report
? technicalCrawlActions({
crawlReportId: technicalBaseline.report.id,
topFixes: completeTopFixes(technicalBaseline.report),
Expand Down Expand Up @@ -897,6 +906,10 @@ function workflowCommandMeta(input: {
input.printFollowups && !full
? compactMainReportJson(report, input.workflowName)
: outputReport
const completeActions = reportActionsWithTechnicalEvidence(
jsonReport.actions,
technicalActions,
)
const reportWithTechnicalEvidence =
!useSearchData && technicalCrawl
? {
Expand All @@ -911,10 +924,7 @@ function workflowCommandMeta(input: {
...exportWorkflowSteps(exportSummaryInput),
...jsonReport.steps,
],
// A provider-free report must surface crawl findings as its
// action queue; an empty list here previously read as "the
// tool found nothing" while the findings sat below.
...(urlModeActions ? { actions: urlModeActions } : {}),
actions: completeActions,
// The compact URL-mode report replaces the empty narrative
// shell (every section unavailable) with an explicit skip
// marker. --full keeps the whole narrative object.
Expand All @@ -930,7 +940,7 @@ function workflowCommandMeta(input: {
},
}),
}
: jsonReport
: { ...jsonReport, actions: completeActions }
const completeReport =
technicalCrawl || followups
? {
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/commands/workflows/url-report-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,10 +529,10 @@ function byPriority(left: PrefixedFix, right: PrefixedFix): number {
}

/**
* Turn the complete crawl issue inventory into workflow actions so a
* provider-free report never returns an empty action list while the crawl
* holds findings. Fixes carry high confidence because the state was
* observed; review observations stay medium because intent is unconfirmed.
* Turn the complete crawl issue inventory into workflow actions so the main
* report does not return an empty or incomplete action list while the crawl
* holds findings. Fixes carry high confidence because the state was observed;
* review observations stay medium because intent is unconfirmed.
*/
export function technicalCrawlActions(input: {
topFixes: TopFix[]
Expand Down