akm-cli 0.9.0-rc.9 → 0.9.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.
- package/CHANGELOG.md +1063 -44
- package/README.md +51 -25
- package/SECURITY.md +14 -1
- package/STABILITY.md +497 -0
- package/dist/akm +148 -35
- package/dist/{akm-migrate-storage → akm-migrate} +6 -9
- package/dist/assets/hints/cli-hints-full.md +223 -95
- package/dist/assets/hints/cli-hints-short.md +85 -22
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/stash-skeleton/README.md +6 -5
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
- package/dist/assets/tasks/core/extract.yml +1 -1
- package/dist/assets/tasks/core/version-check.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
- package/dist/assets/templates/html/health.html +1 -3
- package/dist/assets/workflows/workflow-template.md +32 -15
- package/dist/cli/invocation.js +40 -15
- package/dist/cli/parse-args.js +0 -22
- package/dist/cli/retired-commands.js +121 -0
- package/dist/cli/shared.js +154 -22
- package/dist/cli/unknown-flags.js +236 -0
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +696 -258
- package/dist/commands/agent/agent-dispatch.js +14 -3
- package/dist/commands/agent/contribute-cli.js +73 -88
- package/dist/commands/completions.js +79 -22
- package/dist/commands/config-cli.js +17 -150
- package/dist/commands/env/env-cli.js +59 -143
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +36 -66
- package/dist/commands/env/secret.js +24 -57
- package/dist/commands/feedback-cli.js +141 -87
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +3 -4
- package/dist/commands/health/checks.js +85 -23
- package/dist/commands/health/html-report.js +7 -10
- package/dist/commands/health/improve-metrics.js +25 -83
- package/dist/commands/health/md-report.js +5 -9
- package/dist/commands/health/metrics.js +62 -20
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +4 -5
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +3 -48
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/commands/health/types-improve.js +7 -0
- package/dist/commands/health.js +99 -28
- package/dist/commands/improve/anti-collapse.js +2 -2
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +41 -40
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +140 -1000
- package/dist/commands/improve/distill/promote-memory.js +12 -12
- package/dist/commands/improve/distill/quality-gate.js +6 -6
- package/dist/commands/improve/distill.js +58 -69
- package/dist/commands/improve/eligibility.js +95 -55
- package/dist/commands/improve/extract-cli.js +14 -133
- package/dist/commands/improve/improve-cli.js +98 -114
- package/dist/commands/improve/improve-result-file.js +1 -28
- package/dist/commands/improve/improve-strategies.js +8 -5
- package/dist/commands/improve/improve.js +128 -91
- package/dist/commands/improve/loop-stages.js +182 -20
- package/dist/commands/improve/memory/derived-ref.js +45 -43
- package/dist/commands/improve/memory/memory-belief.js +1 -1
- package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
- package/dist/commands/improve/memory/memory-improve.js +6 -5
- package/dist/commands/improve/outcome-loop.js +22 -65
- package/dist/commands/improve/preparation.js +114 -123
- package/dist/commands/improve/proactive-maintenance.js +2 -5
- package/dist/commands/improve/reflect.js +56 -160
- package/dist/commands/improve/salience.js +11 -122
- package/dist/commands/improve/source-identity.js +10 -38
- package/dist/commands/lint/base-linter.js +20 -124
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +249 -43
- package/dist/commands/{events.js → log.js} +33 -38
- package/dist/commands/migrate-cli.js +92 -12
- package/dist/commands/migration-tool.js +46 -0
- package/dist/commands/observability-cli.js +70 -209
- package/dist/commands/proposal/drain.js +101 -29
- package/dist/commands/proposal/proposal-cli.js +76 -48
- package/dist/commands/proposal/proposal.js +54 -18
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +23 -15
- package/dist/commands/proposal/repository.js +701 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
- package/dist/commands/proposal/validators/proposal-validators.js +55 -7
- package/dist/commands/proposal/validators/proposals.js +4 -7
- package/dist/commands/read/curate.js +34 -53
- package/dist/commands/read/knowledge.js +150 -95
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +42 -15
- package/dist/commands/read/search-cli.js +180 -78
- package/dist/commands/read/search.js +58 -43
- package/dist/commands/read/show.js +197 -141
- package/dist/commands/registry-cli.js +12 -51
- package/dist/commands/remember.js +14 -57
- package/dist/commands/sources/add-cli.js +100 -31
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +7 -2
- package/dist/commands/sources/info.js +18 -5
- package/dist/commands/sources/init.js +12 -12
- package/dist/commands/sources/installed-stashes.js +382 -98
- package/dist/commands/sources/schema-repair.js +3 -2
- package/dist/commands/sources/self-update.js +131 -38
- package/dist/commands/sources/source-add.js +72 -17
- package/dist/commands/sources/source-clone.js +129 -45
- package/dist/commands/sources/source-manage.js +43 -23
- package/dist/commands/sources/sources-cli.js +57 -208
- package/dist/commands/sources/stash-cli.js +46 -53
- package/dist/commands/tasks/tasks-cli.js +91 -97
- package/dist/commands/tasks/tasks.js +276 -421
- package/dist/commands/workflow-cli.js +175 -450
- package/dist/core/adapter/adapters/akm-adapter.js +47 -28
- package/dist/core/adapter/adapters/akm-lint.js +42 -27
- package/dist/core/adapter/adapters/akm-metadata.js +15 -44
- package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
- package/dist/core/adapter/adapters/index.js +6 -6
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
- package/dist/core/adapter/adapters/okf-adapter.js +187 -19
- package/dist/core/adapter/adapters/shared.js +3 -19
- package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +6 -4
- package/dist/core/adapter/validate-context.js +214 -0
- package/dist/core/asset/akm-markdown.js +63 -0
- package/dist/core/asset/asset-placement.js +20 -6
- package/dist/core/asset/asset-ref.js +11 -9
- package/dist/core/asset/frontmatter-lint.js +30 -0
- package/dist/core/asset/frontmatter.js +25 -9
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +89 -18
- package/dist/core/asset/stash-meta.js +1 -1
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +152 -38
- package/dist/core/config/config-io.js +12 -1
- package/dist/core/config/config-schema.js +35 -8
- package/dist/core/config/config-sources.js +55 -11
- package/dist/core/config/config-walker.js +25 -9
- package/dist/core/config/config.js +9 -48
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +5 -1
- package/dist/core/config/schema/experimental.js +30 -0
- package/dist/core/config/schema/improve-processes.js +0 -6
- package/dist/core/config/schema/improve.js +21 -3
- package/dist/core/config/schema/index-config.js +8 -15
- package/dist/core/config/schema/output.js +4 -1
- package/dist/core/config/schema/setup.js +9 -18
- package/dist/core/config/schema/sources-bundles.js +49 -33
- package/dist/core/config/schema/workflow.js +3 -3
- package/dist/core/env-secret-ref.js +76 -46
- package/dist/core/errors.js +18 -12
- package/dist/core/events.js +46 -128
- package/dist/core/file-change.js +6 -5
- package/dist/core/fs-txn.js +83 -7
- package/dist/core/git-message.js +2 -2
- package/dist/core/improve-result.js +1 -100
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +2 -1
- package/dist/core/migration-operation.js +16 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/parse.js +4 -1
- package/dist/core/paths.js +17 -20
- package/dist/core/recognition-util.js +12 -14
- package/dist/core/redaction.js +34 -0
- package/dist/core/standards/resolve-standards-context.js +2 -14
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +2 -2
- package/dist/core/state/migrations.js +41 -18
- package/dist/core/state-db.js +5 -14
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +6 -4
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/type-presentation.js +3 -3
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +771 -95
- package/dist/indexer/bundle-identity-guard.js +3 -2
- package/dist/indexer/db/graph-db.js +0 -24
- package/dist/indexer/ensure-index.js +1 -0
- package/dist/indexer/graph/graph-boost.js +9 -34
- package/dist/indexer/graph/graph-extraction.js +8 -5
- package/dist/indexer/index-writer-lock.js +53 -17
- package/dist/indexer/index-written-assets.js +16 -22
- package/dist/indexer/indexer.js +497 -239
- package/dist/indexer/installations.js +14 -96
- package/dist/indexer/passes/dir-staleness.js +16 -9
- package/dist/indexer/passes/memory-inference.js +11 -9
- package/dist/indexer/passes/metadata.js +113 -47
- package/dist/indexer/scan/doc-to-entry.js +38 -1
- package/dist/indexer/scan/drain-dir.js +13 -23
- package/dist/indexer/search/db-search.js +99 -54
- package/dist/indexer/search/fts-query.js +47 -24
- package/dist/indexer/search/ranking-contributors.js +42 -20
- package/dist/indexer/search/ranking.js +18 -99
- package/dist/indexer/search/search-fields.js +7 -2
- package/dist/indexer/search/search-source.js +82 -93
- package/dist/indexer/usage/usage-events.js +0 -89
- package/dist/indexer/walk/file-context.js +2 -1
- package/dist/indexer/walk/matchers.js +30 -43
- package/dist/indexer/walk/path-resolver.js +7 -2
- package/dist/indexer/walk/walker.js +38 -12
- package/dist/integrations/agent/builders.js +0 -6
- package/dist/integrations/agent/config.js +2 -2
- package/dist/integrations/agent/detect.js +49 -19
- package/dist/integrations/agent/engine-fallback.js +76 -0
- package/dist/integrations/agent/profiles.js +14 -0
- package/dist/integrations/agent/prompts.js +12 -8
- package/dist/integrations/agent/runner-dispatch.js +4 -2
- package/dist/integrations/agent/runner.js +0 -1
- package/dist/integrations/agent/spawn.js +5 -6
- package/dist/integrations/github.js +1 -1
- package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
- package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
- package/dist/integrations/harnesses/claude/session-log.js +0 -10
- package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
- package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
- package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
- package/dist/integrations/harnesses/index.js +3 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
- package/dist/integrations/harnesses/opencode/session-log.js +0 -15
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
- package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
- package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
- package/dist/integrations/lockfile.js +88 -7
- package/dist/integrations/session-logs/index.js +3 -28
- package/dist/llm/client.js +136 -100
- package/dist/llm/embedders/remote.js +13 -5
- package/dist/llm/feature-gate.js +4 -12
- package/dist/llm/graph-extract.js +5 -11
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +5 -7
- package/dist/llm/structured-call.js +1 -1
- package/dist/llm/usage-persist.js +26 -5
- package/dist/llm/usage-telemetry.js +25 -2
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +259 -0
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +14 -36
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +56 -83
- package/dist/output/shapes/migrate.js +8 -0
- package/dist/output/shapes/passthrough.js +7 -41
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes.js +2 -9
- package/dist/output/text/{init.js → bundle-create.js} +3 -1
- package/dist/output/text/bundle-show.js +7 -0
- package/dist/output/text/command-format.js +164 -96
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/health-format.js +103 -0
- package/dist/output/text/health.js +7 -0
- package/dist/output/text/helpers.js +10 -8
- package/dist/output/text/lint-format.js +43 -0
- package/dist/output/text/{save.js → lint.js} +2 -2
- package/dist/output/text/migrate.js +88 -0
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +44 -72
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/show-directives.js +15 -7
- package/dist/output/text/status-list.js +32 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +24 -203
- package/dist/output/text/workflow.js +1 -7
- package/dist/output/text.js +16 -17
- package/dist/registry/factory.js +4 -6
- package/dist/registry/origin-resolve.js +16 -27
- package/dist/registry/providers/skills-sh.js +3 -3
- package/dist/registry/providers/static-index.js +13 -23
- package/dist/registry/resolve.js +42 -7
- package/dist/registry/semver.js +34 -84
- package/dist/runtime.js +2 -23
- package/dist/scripts/akm-migrate-node.js +60290 -0
- package/dist/scripts/akm-migrate.js +59628 -0
- package/dist/setup/detect.js +42 -15
- package/dist/setup/registry-stash-loader.js +2 -2
- package/dist/setup/setup.js +236 -136
- package/dist/setup/steps/connection.js +7 -9
- package/dist/setup/steps/platforms.js +9 -9
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +12 -13
- package/dist/setup/steps/stashdir.js +2 -3
- package/dist/setup/steps/tasks.js +237 -120
- package/dist/sources/freshness.js +1 -1
- package/dist/sources/provider-factory.js +11 -17
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +278 -34
- package/dist/sources/providers/git-provider.js +25 -23
- package/dist/sources/providers/git-stash.js +395 -106
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +7 -4
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +6 -1
- package/dist/sources/resolve.js +6 -5
- package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
- package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
- package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
- package/dist/sources/snapshot-fetchers/github.js +100 -0
- package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
- package/dist/sources/snapshot-fetchers/registry.js +17 -1
- package/dist/sources/snapshot-fetchers/robots.js +348 -0
- package/dist/sources/snapshot-fetchers/rss.js +282 -0
- package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
- package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
- package/dist/sources/snapshot-fetchers/x.js +910 -0
- package/dist/storage/database.js +7 -0
- package/dist/storage/engines/sqlite-migrations.js +23 -111
- package/dist/storage/managed-db.js +2 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +27 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -12
- package/dist/storage/repositories/index-connection.js +17 -6
- package/dist/storage/repositories/index-entries-repository.js +151 -240
- package/dist/storage/repositories/index-entry-mapper.js +15 -11
- package/dist/storage/repositories/index-fts-repository.js +5 -2
- package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
- package/dist/storage/repositories/index-meta-repository.js +2 -3
- package/dist/storage/repositories/index-schema.js +10 -25
- package/dist/storage/repositories/index-utility-repository.js +15 -28
- package/dist/storage/repositories/index-vec-repository.js +6 -1
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +296 -59
- package/dist/storage/repositories/registry-cache.js +19 -0
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +15 -13
- package/dist/storage/repositories/workflow-runs-repository.js +52 -40
- package/dist/tasks/backends/cron.js +105 -15
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +85 -38
- package/dist/tasks/backends/schtasks.js +135 -15
- package/dist/tasks/embedded.js +56 -40
- package/dist/tasks/parser.js +7 -157
- package/dist/tasks/resolve-akm-bin.js +137 -59
- package/dist/tasks/runner.js +79 -42
- package/dist/tasks/scheduler-invocation.js +220 -10
- package/dist/tasks/schema.js +24 -1
- package/dist/tasks/task-id.js +1 -3
- package/dist/tasks/validator.js +20 -6
- package/dist/workflows/authoring/authoring.js +94 -143
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/exec/frozen-judge.js +28 -2
- package/dist/workflows/exec/native-executor.js +77 -57
- package/dist/workflows/exec/param-secrets.js +9 -9
- package/dist/workflows/exec/run-workflow.js +133 -79
- package/dist/workflows/exec/step-work.js +219 -346
- package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
- package/dist/workflows/ir/compile.js +141 -270
- package/dist/workflows/ir/freeze.js +40 -30
- package/dist/workflows/ir/params.js +135 -11
- package/dist/workflows/ir/plan-hash.js +1 -1
- package/dist/workflows/ir/schema.js +25 -26
- package/dist/workflows/parser.js +872 -307
- package/dist/workflows/program/expressions.js +20 -208
- package/dist/workflows/program/schema.js +7 -10
- package/dist/workflows/renderer.js +95 -68
- package/dist/workflows/resource-limits.js +2 -0
- package/dist/workflows/runtime/checkin.js +3 -3
- package/dist/workflows/runtime/plan-classifier.js +16 -75
- package/dist/workflows/runtime/runs.js +186 -127
- package/dist/workflows/runtime/unit-checkin.js +1 -1
- package/dist/workflows/runtime/unit-phases.js +2 -2
- package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +30 -36
- package/dist/workflows/validator.js +21 -62
- package/docs/README.md +68 -0
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +11 -11
- package/docs/migration/release-notes/0.9.0.md +208 -27
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +564 -208
- package/docs/migration/v0.9.0-troubleshooting.md +561 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/cli.md +2253 -0
- package/docs/reference/configuration.md +358 -0
- package/docs/reference/data-and-telemetry.md +105 -42
- package/docs/reference/workflows.md +647 -0
- package/package.json +22 -11
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +81 -128
- package/schemas/akm-workflow.json +74 -73
- package/dist/assets/tasks/core/backup.yml +0 -5
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/cli/config-migrate.js +0 -1878
- package/dist/cli/config-validate.js +0 -41
- package/dist/commands/backup-cli.js +0 -56
- package/dist/commands/bundle/bundle-cli.js +0 -68
- package/dist/commands/bundle/bundle.js +0 -219
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -489
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/mv-cli.js +0 -1221
- package/dist/commands/sources/history.js +0 -201
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/core/migration-backup.js +0 -1231
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/migrate/legacy/config-source-migration.js +0 -257
- package/dist/migrate/legacy/content-migration.js +0 -350
- package/dist/migrate/legacy/legacy-layout.js +0 -779
- package/dist/migrate/legacy/legacy-paths.js +0 -25
- package/dist/migrate/legacy/legacy-stash-json.js +0 -80
- package/dist/migrate/legacy/proposal-fs-import.js +0 -168
- package/dist/migrate/legacy/task-target-ref-migration.js +0 -278
- package/dist/migrate/legacy/three-db-cutover.js +0 -845
- package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
- package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
- package/dist/migrate/legacy-ref-grammar.js +0 -214
- package/dist/output/shapes/distill.js +0 -14
- package/dist/output/shapes/history.js +0 -11
- package/dist/output/text/distill.js +0 -6
- package/dist/output/text/enable-disable.js +0 -8
- package/dist/output/text/history.js +0 -6
- package/dist/registry/build-index.js +0 -382
- package/dist/schemas/akm-config.json +0 -4704
- package/dist/schemas/akm-task.json +0 -87
- package/dist/schemas/akm-workflow.json +0 -372
- package/dist/scripts/migrate-storage.js +0 -3816
- package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
- package/dist/workflows/cli.js +0 -53
- package/dist/workflows/exec/brief.js +0 -481
- package/dist/workflows/exec/report.js +0 -1460
- package/dist/workflows/exec/watch.js +0 -116
- package/dist/workflows/program/parser.js +0 -813
- package/dist/workflows/program/project.js +0 -104
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { parse } from "node-html-parser";
|
|
5
|
+
import TurndownService from "turndown";
|
|
6
|
+
import { escapeMarkdownStructure } from "./fetcher-util.js";
|
|
7
|
+
/**
|
|
8
|
+
* Main-content extraction and HTML -> Markdown conversion for website snapshots.
|
|
9
|
+
*
|
|
10
|
+
* Snapshots are read back by agents as trusted knowledge, so anything a hostile
|
|
11
|
+
* page can smuggle into the markdown is a prompt-injection vector. Three
|
|
12
|
+
* invariants are load-bearing and each is enforced defensively:
|
|
13
|
+
*
|
|
14
|
+
* 1. `<script>` / `<style>` / `<noscript>` / `<template>` bodies never reach
|
|
15
|
+
* the markdown.
|
|
16
|
+
* 2. Only `http:` / `https:` URLs are emitted as links or images; anything
|
|
17
|
+
* else degrades to plain text.
|
|
18
|
+
* 3. A page cannot forge document structure — code fences are sized so their
|
|
19
|
+
* content cannot close them early.
|
|
20
|
+
*/
|
|
21
|
+
const DANGEROUS_TAGS = ["script", "style", "noscript", "template"];
|
|
22
|
+
/**
|
|
23
|
+
* Scrub patterns for {@link scrubDangerousMarkup}, compiled once at module
|
|
24
|
+
* load rather than per call — this runs on every crawled page.
|
|
25
|
+
*/
|
|
26
|
+
const DANGEROUS_TAG_PATTERNS = DANGEROUS_TAGS.map((tag) => ({
|
|
27
|
+
// A full block with any end-tag spelling a browser would accept.
|
|
28
|
+
full: new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/\\s*${tag}\\b[^>]*>`, "gi"),
|
|
29
|
+
// Unterminated open tag: everything to EOF is inside the raw-text element.
|
|
30
|
+
unterminated: new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*$`, "i"),
|
|
31
|
+
// Any stray leftover tag of this kind.
|
|
32
|
+
stray: new RegExp(`<\\/?\\s*${tag}\\b[^>]*>`, "gi"),
|
|
33
|
+
}));
|
|
34
|
+
const PAGE_CHROME_SELECTORS = ["nav", "header", "footer", "aside"];
|
|
35
|
+
/** Inform-parity classed chrome removed from whichever content region wins. */
|
|
36
|
+
const UNWANTED_CLASS_SELECTORS = [
|
|
37
|
+
".nav",
|
|
38
|
+
".navigation",
|
|
39
|
+
".menu",
|
|
40
|
+
".sidebar",
|
|
41
|
+
".advertisement",
|
|
42
|
+
".ad",
|
|
43
|
+
".social",
|
|
44
|
+
".share",
|
|
45
|
+
".comments",
|
|
46
|
+
".related",
|
|
47
|
+
".breadcrumb",
|
|
48
|
+
".breadcrumbs",
|
|
49
|
+
".cookie-notice",
|
|
50
|
+
".popup",
|
|
51
|
+
".modal",
|
|
52
|
+
".overlay",
|
|
53
|
+
];
|
|
54
|
+
/** Content-region selectors in priority order; narrow regions beat app shells. */
|
|
55
|
+
const CONTENT_SELECTORS = [
|
|
56
|
+
".markdown-body",
|
|
57
|
+
".article-content",
|
|
58
|
+
".entry-content",
|
|
59
|
+
".post-content",
|
|
60
|
+
".docs-content",
|
|
61
|
+
".main-content",
|
|
62
|
+
"#docs-content",
|
|
63
|
+
"#main-content",
|
|
64
|
+
'[role="main"]',
|
|
65
|
+
"main",
|
|
66
|
+
"article",
|
|
67
|
+
"#content",
|
|
68
|
+
".content",
|
|
69
|
+
];
|
|
70
|
+
const ARTICLE_METADATA_SELECTORS = new Set([".article-content", ".entry-content", ".post-content"]);
|
|
71
|
+
/**
|
|
72
|
+
* Remove raw-text blocks textually, BEFORE the DOM parse.
|
|
73
|
+
*
|
|
74
|
+
* This is the security boundary, not `querySelectorAll("script").remove()`.
|
|
75
|
+
* `node-html-parser` only closes a raw-text element on an exactly-matching
|
|
76
|
+
* lowercase `</script>`; browsers additionally accept `</SCRIPT>`,
|
|
77
|
+
* `</script >`, `</script/>`, `</script foo>` and `</ script>`. On any of
|
|
78
|
+
* those the parser never creates a `script` element at all and serializes the
|
|
79
|
+
* script *body* as ordinary text — so DOM-level removal silently does nothing
|
|
80
|
+
* and the payload lands in the snapshot while the page renders normally in a
|
|
81
|
+
* browser. An unterminated `<script>` at EOF has the same effect.
|
|
82
|
+
*/
|
|
83
|
+
function scrubDangerousMarkup(html) {
|
|
84
|
+
let out = html;
|
|
85
|
+
for (const pattern of DANGEROUS_TAG_PATTERNS) {
|
|
86
|
+
out = out.replace(pattern.full, " ").replace(pattern.unterminated, " ").replace(pattern.stray, " ");
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function stripDangerousNodes(root) {
|
|
91
|
+
for (const tag of DANGEROUS_TAGS) {
|
|
92
|
+
for (const node of root.querySelectorAll(tag))
|
|
93
|
+
node.remove();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function isExplicitlyHidden(node) {
|
|
97
|
+
if (node.hasAttribute("hidden") || node.hasAttribute("inert"))
|
|
98
|
+
return true;
|
|
99
|
+
if (node.getAttribute("aria-hidden")?.toLowerCase() === "true")
|
|
100
|
+
return true;
|
|
101
|
+
const style = node.getAttribute("style")?.toLowerCase();
|
|
102
|
+
if (!style)
|
|
103
|
+
return false;
|
|
104
|
+
return style.split(";").some((declaration) => {
|
|
105
|
+
const [property, value] = declaration.split(":", 2).map((part) => part?.trim());
|
|
106
|
+
return ((property === "display" && value?.startsWith("none")) ||
|
|
107
|
+
(property === "visibility" && value?.startsWith("hidden")));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function isHiddenRegion(node) {
|
|
111
|
+
let current = node;
|
|
112
|
+
while (current) {
|
|
113
|
+
if (isExplicitlyHidden(current))
|
|
114
|
+
return true;
|
|
115
|
+
const parentNode = current.parentNode;
|
|
116
|
+
current = parentNode && typeof parentNode.tagName === "string" ? parentNode : null;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
function isInUnwantedRegion(node) {
|
|
121
|
+
let current = node;
|
|
122
|
+
while (current) {
|
|
123
|
+
const tagName = current.tagName.toLowerCase();
|
|
124
|
+
if (PAGE_CHROME_SELECTORS.some((selector) => selector === tagName))
|
|
125
|
+
return true;
|
|
126
|
+
const classes = new Set((current.getAttribute("class") ?? "").split(/\s+/).filter(Boolean));
|
|
127
|
+
if (UNWANTED_CLASS_SELECTORS.some((selector) => classes.has(selector.slice(1))))
|
|
128
|
+
return true;
|
|
129
|
+
const parentNode = current.parentNode;
|
|
130
|
+
current = parentNode && typeof parentNode.tagName === "string" ? parentNode : null;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
function stripUnwantedNodes(root, preserveSemanticChrome = false) {
|
|
135
|
+
for (const node of root.querySelectorAll("*")) {
|
|
136
|
+
if (isExplicitlyHidden(node))
|
|
137
|
+
node.remove();
|
|
138
|
+
}
|
|
139
|
+
for (const selector of PAGE_CHROME_SELECTORS) {
|
|
140
|
+
if (preserveSemanticChrome && selector !== "nav")
|
|
141
|
+
continue;
|
|
142
|
+
for (const node of root.querySelectorAll(selector)) {
|
|
143
|
+
node.remove();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const selector of UNWANTED_CLASS_SELECTORS) {
|
|
147
|
+
for (const node of root.querySelectorAll(selector))
|
|
148
|
+
node.remove();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function enclosingArticle(node) {
|
|
152
|
+
let current = node;
|
|
153
|
+
while (current) {
|
|
154
|
+
if (typeof current.tagName === "string" && current.tagName.toLowerCase() === "article")
|
|
155
|
+
return current;
|
|
156
|
+
const parentNode = current.parentNode;
|
|
157
|
+
current = parentNode && typeof parentNode.tagName === "string" ? parentNode : null;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function hasMatchingAncestor(candidate, matches) {
|
|
162
|
+
let current = candidate.parentNode;
|
|
163
|
+
while (current && typeof current.tagName === "string") {
|
|
164
|
+
if (matches.has(current))
|
|
165
|
+
return true;
|
|
166
|
+
current = current.parentNode;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
/** True for the only link schemes we will emit into agent-facing markdown. */
|
|
171
|
+
export function isSafeLinkUrl(url) {
|
|
172
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Resolve an href/src for emission: absolute, http(s) only, credentials
|
|
176
|
+
* stripped. Returns null when the URL must not be emitted.
|
|
177
|
+
*/
|
|
178
|
+
function resolveEmittableUrl(raw, pageUrl) {
|
|
179
|
+
try {
|
|
180
|
+
const resolved = new URL(raw, pageUrl);
|
|
181
|
+
if (!isSafeLinkUrl(resolved))
|
|
182
|
+
return null;
|
|
183
|
+
// `validateWebsiteUrl` rejects credentials on input URLs; do not let them
|
|
184
|
+
// re-enter through a link inside page content.
|
|
185
|
+
resolved.username = "";
|
|
186
|
+
resolved.password = "";
|
|
187
|
+
return resolved.toString();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Escape characters that would let attacker-controlled text break out of a
|
|
195
|
+
* markdown link/image and start a new one. `alt` and link labels are raw page
|
|
196
|
+
* strings; an alt of `x](javascript:alert(1))` otherwise emits a second,
|
|
197
|
+
* unvalidated destination ahead of the validated `src`.
|
|
198
|
+
*/
|
|
199
|
+
function escapeMarkdownLabel(value) {
|
|
200
|
+
return value
|
|
201
|
+
.replace(/([\\[\]])/g, "\\$1")
|
|
202
|
+
.replace(/\s+/g, " ")
|
|
203
|
+
.trim();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Percent-encode parens in a link destination. Unescaped parens truncate the
|
|
207
|
+
* link and spill the remainder into the document as live markup. Encoding
|
|
208
|
+
* (rather than the `<...>` destination form) keeps the result free of `<`, so
|
|
209
|
+
* {@link escapeResidualMarkup} cannot corrupt it afterwards.
|
|
210
|
+
*/
|
|
211
|
+
function markdownDestination(url) {
|
|
212
|
+
return url.replaceAll("(", "%28").replaceAll(")", "%29");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Guard against pathological nesting. `node-html-parser` and Turndown both
|
|
216
|
+
* recurse, and ~20k nested elements (well under the page byte cap) overflows
|
|
217
|
+
* the stack — which would otherwise abort an entire crawl.
|
|
218
|
+
*/
|
|
219
|
+
const MAX_NESTING_DEPTH = 2_000;
|
|
220
|
+
function exceedsNestingBudget(html) {
|
|
221
|
+
let depth = 0;
|
|
222
|
+
let max = 0;
|
|
223
|
+
const tagPattern = /<(\/?)([a-zA-Z][\w:-]*)[^>]*?(\/?)>/g;
|
|
224
|
+
for (const match of html.matchAll(tagPattern)) {
|
|
225
|
+
const closing = match[1] === "/";
|
|
226
|
+
const selfClosing = match[3] === "/";
|
|
227
|
+
if (selfClosing)
|
|
228
|
+
continue;
|
|
229
|
+
if (closing)
|
|
230
|
+
depth = Math.max(0, depth - 1);
|
|
231
|
+
else {
|
|
232
|
+
depth += 1;
|
|
233
|
+
if (depth > max)
|
|
234
|
+
max = depth;
|
|
235
|
+
if (max > MAX_NESTING_DEPTH)
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
/** Last-resort conversion when the document is too deep to parse safely. */
|
|
242
|
+
function plainTextFallback(html) {
|
|
243
|
+
const text = scrubDangerousMarkup(html)
|
|
244
|
+
.replace(/<[^>]+>/g, " ")
|
|
245
|
+
.replace(/\s+/g, " ")
|
|
246
|
+
.trim();
|
|
247
|
+
return escapeMarkdownStructure(text.replace(/([\\[\]`])/g, "\\$1"));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Select the main-content region of a document and return its HTML.
|
|
251
|
+
*
|
|
252
|
+
* Falls back to `<body>` minus page chrome, then to the whole document, so a
|
|
253
|
+
* page with no semantic markup still yields content rather than nothing.
|
|
254
|
+
*/
|
|
255
|
+
/**
|
|
256
|
+
* Select the content region from an already-parsed, already-scrubbed root
|
|
257
|
+
* (dangerous nodes removed). Mutates the root when it falls back to `<body>`.
|
|
258
|
+
*/
|
|
259
|
+
function selectMainContentFromRoot(root) {
|
|
260
|
+
for (const selector of CONTENT_SELECTORS) {
|
|
261
|
+
const candidates = root
|
|
262
|
+
.querySelectorAll(selector)
|
|
263
|
+
.filter((match) => match.textContent.trim() && !isHiddenRegion(match) && !isInUnwantedRegion(match));
|
|
264
|
+
const candidateSet = new Set(candidates);
|
|
265
|
+
const matches = candidates.filter((match) => !hasMatchingAncestor(match, candidateSet));
|
|
266
|
+
// Repeated cards/articles usually form a listing. Let a broader primary
|
|
267
|
+
// container (or the body fallback) retain all of them instead of silently
|
|
268
|
+
// truncating the page to the first match.
|
|
269
|
+
if (matches.length > 1)
|
|
270
|
+
continue;
|
|
271
|
+
const match = matches[0];
|
|
272
|
+
// Ignore a region that matched structurally but carries no prose — a
|
|
273
|
+
// decorative wrapper should not shadow the real content below it.
|
|
274
|
+
if (match) {
|
|
275
|
+
const region = ARTICLE_METADATA_SELECTORS.has(selector) ? (enclosingArticle(match) ?? match) : match;
|
|
276
|
+
stripUnwantedNodes(region, true);
|
|
277
|
+
if (region.textContent.trim())
|
|
278
|
+
return region.toString();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const body = root.querySelector("body");
|
|
282
|
+
if (body) {
|
|
283
|
+
stripUnwantedNodes(body);
|
|
284
|
+
if (body.textContent.trim())
|
|
285
|
+
return body.toString();
|
|
286
|
+
}
|
|
287
|
+
stripUnwantedNodes(root);
|
|
288
|
+
return root.toString();
|
|
289
|
+
}
|
|
290
|
+
export function extractMainContentHtml(html) {
|
|
291
|
+
const root = parse(scrubDangerousMarkup(html), { comment: false });
|
|
292
|
+
stripDangerousNodes(root);
|
|
293
|
+
return selectMainContentFromRoot(root);
|
|
294
|
+
}
|
|
295
|
+
/** Read a fence language off `class="language-ts"` / `class="lang-ts"`. */
|
|
296
|
+
function fenceLanguage(className) {
|
|
297
|
+
return /(?:language|lang)-([\w+#-]+)/.exec(className)?.[1] ?? "";
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Size a fence so its own content cannot close it. Turndown's default rule
|
|
301
|
+
* does this; the custom rule below must not regress it, or a page can end the
|
|
302
|
+
* fence early and inject forged headings and prose into the snapshot.
|
|
303
|
+
*/
|
|
304
|
+
function fenceFor(code) {
|
|
305
|
+
let longest = 0;
|
|
306
|
+
for (const run of code.matchAll(/`+/g))
|
|
307
|
+
longest = Math.max(longest, run[0].length);
|
|
308
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
309
|
+
}
|
|
310
|
+
function createTurndown(pageUrl) {
|
|
311
|
+
const service = new TurndownService({
|
|
312
|
+
headingStyle: "atx",
|
|
313
|
+
codeBlockStyle: "fenced",
|
|
314
|
+
emDelimiter: "_",
|
|
315
|
+
bulletListMarker: "-",
|
|
316
|
+
});
|
|
317
|
+
// Defense in depth: these are already scrubbed textually and removed from
|
|
318
|
+
// the DOM. A caller converting raw HTML must not be able to leak them either.
|
|
319
|
+
service.remove([...DANGEROUS_TAGS]);
|
|
320
|
+
service.addRule("fencedCodeBlock", {
|
|
321
|
+
filter: (node) => node.nodeName === "PRE" && Boolean(node.firstChild) && node.firstChild?.nodeName === "CODE",
|
|
322
|
+
replacement: (_content, node) => {
|
|
323
|
+
const code = node.firstChild;
|
|
324
|
+
const language = fenceLanguage(code?.getAttribute?.("class") ?? "");
|
|
325
|
+
const text = (code?.textContent ?? "").replace(/\n+$/, "");
|
|
326
|
+
if (!text.trim())
|
|
327
|
+
return "\n\n";
|
|
328
|
+
const fence = fenceFor(text);
|
|
329
|
+
return `\n\n${fence}${language}\n${text}\n${fence}\n\n`;
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
service.addRule("safeLink", {
|
|
333
|
+
filter: (node) => node.nodeName === "A",
|
|
334
|
+
replacement: (content, node) => {
|
|
335
|
+
const label = content.trim();
|
|
336
|
+
if (!label)
|
|
337
|
+
return "";
|
|
338
|
+
const href = node.getAttribute("href");
|
|
339
|
+
if (!href)
|
|
340
|
+
return label;
|
|
341
|
+
const resolved = resolveEmittableUrl(href, pageUrl);
|
|
342
|
+
if (!resolved)
|
|
343
|
+
return label;
|
|
344
|
+
return `[${escapeMarkdownLabel(label)}](${markdownDestination(resolved)})`;
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
// Turndown's built-in image rule emits `src` verbatim — no scheme check and
|
|
348
|
+
// no base resolution — so `javascript:` / `data:` srcs would slip past the
|
|
349
|
+
// anchor policy entirely.
|
|
350
|
+
service.addRule("safeImage", {
|
|
351
|
+
filter: (node) => node.nodeName === "IMG",
|
|
352
|
+
replacement: (_content, node) => {
|
|
353
|
+
const element = node;
|
|
354
|
+
const alt = escapeMarkdownLabel(element.getAttribute("alt") ?? "");
|
|
355
|
+
const src = element.getAttribute("src");
|
|
356
|
+
if (!src)
|
|
357
|
+
return alt;
|
|
358
|
+
const resolved = resolveEmittableUrl(src, pageUrl);
|
|
359
|
+
return resolved ? `})` : alt;
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
const tableSupport = new WeakMap();
|
|
363
|
+
const tableFor = (node) => {
|
|
364
|
+
let current = node;
|
|
365
|
+
while (current) {
|
|
366
|
+
if (current.nodeName === "TABLE")
|
|
367
|
+
return current;
|
|
368
|
+
current = current.parentNode;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
};
|
|
372
|
+
const cellsFor = (row) => Array.from(row.childNodes).filter((child) => child.nodeName === "TH" || child.nodeName === "TD");
|
|
373
|
+
const hasUnsupportedSpan = (cell) => {
|
|
374
|
+
const colspan = cell.getAttribute("colspan");
|
|
375
|
+
const rowspan = cell.getAttribute("rowspan");
|
|
376
|
+
return (colspan !== null && colspan !== "1") || (rowspan !== null && rowspan !== "1");
|
|
377
|
+
};
|
|
378
|
+
const isSupportedTable = (table) => {
|
|
379
|
+
const cached = tableSupport.get(table);
|
|
380
|
+
if (cached !== undefined)
|
|
381
|
+
return cached;
|
|
382
|
+
const hasAncestorTable = table.parentNode !== null && tableFor(table.parentNode) !== null;
|
|
383
|
+
let ancestor = table.parentNode;
|
|
384
|
+
while (ancestor) {
|
|
385
|
+
if (ancestor.nodeName === "TABLE") {
|
|
386
|
+
tableSupport.set(table, false);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
ancestor = ancestor.parentNode;
|
|
390
|
+
}
|
|
391
|
+
const rows = [];
|
|
392
|
+
let hasCaption = false;
|
|
393
|
+
let hasNestedTable = false;
|
|
394
|
+
const visit = (node) => {
|
|
395
|
+
for (const child of Array.from(node.childNodes)) {
|
|
396
|
+
if (child.nodeName === "TABLE") {
|
|
397
|
+
hasNestedTable = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (child.nodeName === "CAPTION")
|
|
401
|
+
hasCaption = true;
|
|
402
|
+
if (child.nodeName === "TR")
|
|
403
|
+
rows.push(child);
|
|
404
|
+
visit(child);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
visit(table);
|
|
408
|
+
const firstCells = rows[0] ? cellsFor(rows[0]) : [];
|
|
409
|
+
const supported = rows.length > 0 &&
|
|
410
|
+
!hasAncestorTable &&
|
|
411
|
+
!hasCaption &&
|
|
412
|
+
!hasNestedTable &&
|
|
413
|
+
firstCells.length > 0 &&
|
|
414
|
+
firstCells.every((cell) => cell.nodeName === "TH" && !hasUnsupportedSpan(cell)) &&
|
|
415
|
+
rows.slice(1).every((row) => {
|
|
416
|
+
const cells = cellsFor(row);
|
|
417
|
+
return (cells.length === firstCells.length &&
|
|
418
|
+
cells.every((cell) => cell.nodeName === "TD" && !hasUnsupportedSpan(cell)));
|
|
419
|
+
});
|
|
420
|
+
tableSupport.set(table, supported);
|
|
421
|
+
return supported;
|
|
422
|
+
};
|
|
423
|
+
const isInSupportedTable = (node) => {
|
|
424
|
+
const table = tableFor(node);
|
|
425
|
+
return table !== null && isSupportedTable(table);
|
|
426
|
+
};
|
|
427
|
+
service.addRule("tableCell", {
|
|
428
|
+
filter: (node) => (node.nodeName === "TH" || node.nodeName === "TD") && isInSupportedTable(node),
|
|
429
|
+
replacement: (content) => {
|
|
430
|
+
const cell = content
|
|
431
|
+
.replace(/\|/g, "\\|")
|
|
432
|
+
.replace(/\r?\n+/g, " ")
|
|
433
|
+
.trim();
|
|
434
|
+
return ` ${cell} |`;
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
service.addRule("tableRow", {
|
|
438
|
+
filter: (node) => node.nodeName === "TR" && isInSupportedTable(node),
|
|
439
|
+
replacement: (content, node) => {
|
|
440
|
+
const cells = Array.from(node.childNodes).filter((child) => child.nodeName === "TH" || child.nodeName === "TD");
|
|
441
|
+
if (cells.length === 0)
|
|
442
|
+
return "";
|
|
443
|
+
const row = `|${content.trimEnd()}\n`;
|
|
444
|
+
const isHeader = cells.every((cell) => cell.nodeName === "TH");
|
|
445
|
+
return isHeader ? `${row}|${cells.map(() => " --- |").join("")}\n` : row;
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
service.addRule("table", {
|
|
449
|
+
filter: (node) => node.nodeName === "TABLE" && isInSupportedTable(node),
|
|
450
|
+
replacement: (content) => `\n\n${content.trim().replace(/\n\s*\n/g, "\n")}\n\n`,
|
|
451
|
+
});
|
|
452
|
+
return service;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Escape markup that survives conversion as literal text.
|
|
456
|
+
*
|
|
457
|
+
* Turndown does not escape `<`, so page text written as `<script>`
|
|
458
|
+
* (harmless, visible text on the page) would otherwise become a live
|
|
459
|
+
* `<script>` tag in the markdown and execute in any renderer that passes raw
|
|
460
|
+
* HTML through.
|
|
461
|
+
*/
|
|
462
|
+
function escapeResidualMarkup(markdown) {
|
|
463
|
+
// Only `<` that begins a tag-like construct; a bare `a < b` stays readable.
|
|
464
|
+
return markdown.replace(/<(?=[a-zA-Z/!?])/g, "<");
|
|
465
|
+
}
|
|
466
|
+
/** Escape residual markup, then normalize whitespace, into the final snapshot. */
|
|
467
|
+
function finalizeMarkdown(markdown) {
|
|
468
|
+
return escapeResidualMarkup(markdown)
|
|
469
|
+
.replace(/\r/g, "")
|
|
470
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
471
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
472
|
+
.trim();
|
|
473
|
+
}
|
|
474
|
+
/** Turn an already-scrubbed root into snapshot Markdown (mutates the root). */
|
|
475
|
+
function markdownFromRoot(root, html, pageUrl) {
|
|
476
|
+
if (exceedsNestingBudget(html))
|
|
477
|
+
return plainTextFallback(html);
|
|
478
|
+
try {
|
|
479
|
+
stripDangerousNodes(root);
|
|
480
|
+
return createTurndown(pageUrl).turndown(selectMainContentFromRoot(root));
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// Depth budget is a heuristic; a parser or converter blow-up must
|
|
484
|
+
// degrade this page, never abort the surrounding crawl.
|
|
485
|
+
return plainTextFallback(html);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Collect http(s) links from an already-parsed, already-scrubbed root. Read
|
|
490
|
+
* BEFORE any content-region mutation so nav/header/footer links survive.
|
|
491
|
+
*/
|
|
492
|
+
function collectLinksFromRoot(root, pageUrl) {
|
|
493
|
+
const links = [];
|
|
494
|
+
for (const anchor of root.querySelectorAll("a")) {
|
|
495
|
+
const href = anchor.getAttribute("href")?.trim();
|
|
496
|
+
if (!href || href.startsWith("#"))
|
|
497
|
+
continue;
|
|
498
|
+
try {
|
|
499
|
+
const resolved = new URL(href, pageUrl);
|
|
500
|
+
if (isSafeLinkUrl(resolved))
|
|
501
|
+
links.push(resolved);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
/* ignore malformed hrefs */
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return links;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Convert a page to Markdown, scoped to its main-content region.
|
|
511
|
+
*
|
|
512
|
+
* `pageUrl` resolves relative hrefs; it is not fetched.
|
|
513
|
+
*/
|
|
514
|
+
export function htmlToMarkdown(html, pageUrl) {
|
|
515
|
+
let markdown;
|
|
516
|
+
if (exceedsNestingBudget(html)) {
|
|
517
|
+
markdown = plainTextFallback(html);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
try {
|
|
521
|
+
markdown = createTurndown(pageUrl).turndown(extractMainContentHtml(html));
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
markdown = plainTextFallback(html);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return finalizeMarkdown(markdown);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Crawl-path entry point: convert a page AND collect its links from a SINGLE
|
|
531
|
+
* parse. The content region and the whole-document link set both derive from
|
|
532
|
+
* the same DOM, so a crawled page is scrubbed and parsed once, not twice.
|
|
533
|
+
*
|
|
534
|
+
* Links are collected from the whole document (nav/header/footer included —
|
|
535
|
+
* that is how a crawl discovers pages) and read before the content-region
|
|
536
|
+
* selection mutates the tree.
|
|
537
|
+
*/
|
|
538
|
+
export function htmlToMarkdownAndLinks(html, pageUrl) {
|
|
539
|
+
let root = null;
|
|
540
|
+
try {
|
|
541
|
+
root = parse(scrubDangerousMarkup(html), { comment: false });
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
root = null;
|
|
545
|
+
}
|
|
546
|
+
const links = root ? collectLinksFromRoot(root, pageUrl) : [];
|
|
547
|
+
const markdown = root ? markdownFromRoot(root, html, pageUrl) : plainTextFallback(html);
|
|
548
|
+
return { markdown: finalizeMarkdown(markdown), links };
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Collect links from the WHOLE document, not the extracted content region.
|
|
552
|
+
*
|
|
553
|
+
* Deliberate and load-bearing: nav/header/footer links are how a crawl
|
|
554
|
+
* discovers pages. Narrowing this to the content region would silently shrink
|
|
555
|
+
* every crawl to whatever the first page happens to link inline.
|
|
556
|
+
*/
|
|
557
|
+
export function extractDocumentLinks(html, pageUrl) {
|
|
558
|
+
let root;
|
|
559
|
+
try {
|
|
560
|
+
root = parse(scrubDangerousMarkup(html), { comment: false });
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
return collectLinksFromRoot(root, pageUrl);
|
|
566
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Small shared helpers for the snapshot fetchers.
|
|
6
|
+
*
|
|
7
|
+
* These live in a leaf module (no fetcher or `website-ingest` imports) so the
|
|
8
|
+
* individual fetchers can share them without cycling through the fetcher
|
|
9
|
+
* registry — the same reason the SSRF guards were pulled into `host-guard.ts`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Neutralize markdown structure in attacker-controlled prose. Without this a
|
|
13
|
+
* post/tweet body containing a line starting with `##` forges a section
|
|
14
|
+
* boundary in the snapshot, letting it impersonate content the fetcher
|
|
15
|
+
* vouched for.
|
|
16
|
+
*/
|
|
17
|
+
export function escapeMarkdownStructure(value) {
|
|
18
|
+
return value
|
|
19
|
+
.replace(/\r\n?/g, "\n")
|
|
20
|
+
.split("\n")
|
|
21
|
+
.map((line) => line.replace(/^(\s*)([#>\-*+=~_`]|\d+[.)])/, "$1\\$2"))
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|
|
24
|
+
/** Coerce an unknown JSON value to a string, defaulting to "". */
|
|
25
|
+
export function coerceString(value) {
|
|
26
|
+
return typeof value === "string" ? value : "";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `index` and `log` are OKF reserved structural basenames at every depth — an
|
|
30
|
+
* adapter never indexes them, so an asset written there imports but can never
|
|
31
|
+
* be found by search or show. Remap the trailing segment (`x/index` ->
|
|
32
|
+
* `x/index-content`). Idempotent: a `-content`-suffixed name is left alone.
|
|
33
|
+
*/
|
|
34
|
+
export function avoidReservedBasename(relPath) {
|
|
35
|
+
const segments = relPath.split("/");
|
|
36
|
+
const last = segments[segments.length - 1] ?? "";
|
|
37
|
+
if (last === "index" || last === "log") {
|
|
38
|
+
segments[segments.length - 1] = `${last}-content`;
|
|
39
|
+
}
|
|
40
|
+
return segments.join("/");
|
|
41
|
+
}
|