akm-cli 0.9.0-rc.8 → 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 +105 -57
- 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 +37 -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 +101 -6
- 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 -1806
- 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 -1234
- 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 -223
- package/dist/migrate/legacy/content-migration.js +0 -305
- 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 -72
- package/dist/migrate/legacy/proposal-fs-import.js +0 -168
- package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
- package/dist/migrate/legacy/three-db-cutover.js +0 -841
- 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,910 @@
|
|
|
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 { ResponseTooLargeError, readBodyWithByteCap } from "../../core/common.js";
|
|
6
|
+
import { warn } from "../../core/warn.js";
|
|
7
|
+
import { avoidReservedBasename, coerceString, escapeMarkdownStructure } from "./fetcher-util.js";
|
|
8
|
+
import { fetchGuardedResponse, fetchPinnedJson } from "./host-guard.js";
|
|
9
|
+
import rssFetcher from "./rss.js";
|
|
10
|
+
/**
|
|
11
|
+
* X / Twitter snapshot fetcher.
|
|
12
|
+
*
|
|
13
|
+
* Profiles retain the API-v2 -> RSS-template strategy. Posts first inspect the
|
|
14
|
+
* public page for an embedded Article, then use exact API-v2 lookup when a token
|
|
15
|
+
* is available, and finally use public Open Graph post text. Public page code is
|
|
16
|
+
* never executed; Article fields are read as bounded JSON-compatible strings.
|
|
17
|
+
*/
|
|
18
|
+
const X_HOSTS = new Set(["x.com", "www.x.com", "twitter.com", "www.twitter.com"]);
|
|
19
|
+
const X_API_BASE = "https://api.x.com/2";
|
|
20
|
+
const DEFAULT_TWEET_LIMIT = 50;
|
|
21
|
+
const X_BYTE_CAP = 4 * 1024 * 1024;
|
|
22
|
+
const X_BODY_TIMEOUT_MS = 30_000;
|
|
23
|
+
const X_PAGE_BYTE_CAP = 5 * 1024 * 1024;
|
|
24
|
+
/** Reserved x.com paths that are not user profiles. */
|
|
25
|
+
const RESERVED_X_PATHS = new Set([
|
|
26
|
+
"home",
|
|
27
|
+
"explore",
|
|
28
|
+
"notifications",
|
|
29
|
+
"messages",
|
|
30
|
+
"search",
|
|
31
|
+
"settings",
|
|
32
|
+
"i",
|
|
33
|
+
"intent",
|
|
34
|
+
"share",
|
|
35
|
+
"compose",
|
|
36
|
+
"login",
|
|
37
|
+
"signup",
|
|
38
|
+
"about",
|
|
39
|
+
"tos",
|
|
40
|
+
"privacy",
|
|
41
|
+
]);
|
|
42
|
+
function validXUsername(value) {
|
|
43
|
+
const username = value?.trim().replace(/^@/, "") ?? "";
|
|
44
|
+
if (!/^[A-Za-z0-9_]{1,15}$/.test(username))
|
|
45
|
+
return null;
|
|
46
|
+
if (RESERVED_X_PATHS.has(username.toLowerCase()))
|
|
47
|
+
return null;
|
|
48
|
+
return username;
|
|
49
|
+
}
|
|
50
|
+
function validXId(value) {
|
|
51
|
+
return value && /^[0-9]{1,19}$/.test(value) ? value : null;
|
|
52
|
+
}
|
|
53
|
+
export function extractXResource(url) {
|
|
54
|
+
if (!X_HOSTS.has(url.hostname.toLowerCase()))
|
|
55
|
+
return null;
|
|
56
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
57
|
+
if (segments.length === 1) {
|
|
58
|
+
const username = validXUsername(segments[0]);
|
|
59
|
+
return username ? { kind: "profile", username } : null;
|
|
60
|
+
}
|
|
61
|
+
if (segments.length === 3 && ["status", "statuses"].includes(segments[1]?.toLowerCase() ?? "")) {
|
|
62
|
+
const usernameHint = validXUsername(segments[0]);
|
|
63
|
+
const postId = validXId(segments[2]);
|
|
64
|
+
if (usernameHint && postId)
|
|
65
|
+
return { kind: "status", postId, usernameHint };
|
|
66
|
+
}
|
|
67
|
+
if (segments[0]?.toLowerCase() === "i") {
|
|
68
|
+
const resourceType = segments[1]?.toLowerCase();
|
|
69
|
+
const id = validXId(segments[2]);
|
|
70
|
+
if (segments.length === 3 && resourceType === "status" && id)
|
|
71
|
+
return { kind: "status", postId: id };
|
|
72
|
+
if (segments.length === 3 && resourceType === "article" && id)
|
|
73
|
+
return { kind: "article", articleId: id };
|
|
74
|
+
const webPostId = validXId(segments[3]);
|
|
75
|
+
if (segments.length === 4 && resourceType === "web" && segments[2]?.toLowerCase() === "status" && webPostId) {
|
|
76
|
+
return { kind: "status", postId: webPostId };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
export function extractXUsername(url) {
|
|
82
|
+
const resource = extractXResource(url);
|
|
83
|
+
return resource?.kind === "profile" ? resource.username : null;
|
|
84
|
+
}
|
|
85
|
+
/** Secret ref consulted when `X_BEARER_TOKEN` is not in the environment. */
|
|
86
|
+
export const X_BEARER_TOKEN_SECRET_REF = "secrets/x-bearer-token";
|
|
87
|
+
export function resolveXBearerToken(context) {
|
|
88
|
+
const fromEnv = process.env.X_BEARER_TOKEN?.trim();
|
|
89
|
+
if (fromEnv)
|
|
90
|
+
return fromEnv;
|
|
91
|
+
try {
|
|
92
|
+
return context?.resolveSecret?.(X_BEARER_TOKEN_SECRET_REF)?.trim() || null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const MAX_RELAY_OBJECT_CANDIDATES = 16;
|
|
99
|
+
const EXECUTABLE_SCRIPT_TYPES = new Set([
|
|
100
|
+
"module",
|
|
101
|
+
"application/ecmascript",
|
|
102
|
+
"application/javascript",
|
|
103
|
+
"application/x-ecmascript",
|
|
104
|
+
"application/x-javascript",
|
|
105
|
+
"text/ecmascript",
|
|
106
|
+
"text/javascript",
|
|
107
|
+
"text/javascript1.0",
|
|
108
|
+
"text/javascript1.1",
|
|
109
|
+
"text/javascript1.2",
|
|
110
|
+
"text/javascript1.3",
|
|
111
|
+
"text/javascript1.4",
|
|
112
|
+
"text/javascript1.5",
|
|
113
|
+
"text/jscript",
|
|
114
|
+
"text/livescript",
|
|
115
|
+
"text/x-ecmascript",
|
|
116
|
+
"text/x-javascript",
|
|
117
|
+
]);
|
|
118
|
+
const EXECUTABLE_SCRIPT_LANGUAGES = new Set([
|
|
119
|
+
"ecmascript",
|
|
120
|
+
"javascript",
|
|
121
|
+
"javascript1.0",
|
|
122
|
+
"javascript1.1",
|
|
123
|
+
"javascript1.2",
|
|
124
|
+
"javascript1.3",
|
|
125
|
+
"javascript1.4",
|
|
126
|
+
"javascript1.5",
|
|
127
|
+
"jscript",
|
|
128
|
+
"livescript",
|
|
129
|
+
]);
|
|
130
|
+
const CONTROL_CONDITION_KEYWORDS = new Set(["catch", "for", "if", "switch", "while", "with"]);
|
|
131
|
+
const REGEX_PREFIX_KEYWORDS = new Set([
|
|
132
|
+
"await",
|
|
133
|
+
"case",
|
|
134
|
+
"delete",
|
|
135
|
+
"do",
|
|
136
|
+
"else",
|
|
137
|
+
"in",
|
|
138
|
+
"instanceof",
|
|
139
|
+
"new",
|
|
140
|
+
"of",
|
|
141
|
+
"return",
|
|
142
|
+
"throw",
|
|
143
|
+
"typeof",
|
|
144
|
+
"void",
|
|
145
|
+
"yield",
|
|
146
|
+
]);
|
|
147
|
+
function closesControlCondition(source, code, closeIndex) {
|
|
148
|
+
let depth = 0;
|
|
149
|
+
for (let i = closeIndex; i >= 0; i--) {
|
|
150
|
+
if (code[i] !== 1)
|
|
151
|
+
continue;
|
|
152
|
+
const char = source[i] ?? "";
|
|
153
|
+
if (char === ")")
|
|
154
|
+
depth += 1;
|
|
155
|
+
else if (char === "(") {
|
|
156
|
+
depth -= 1;
|
|
157
|
+
if (depth !== 0)
|
|
158
|
+
continue;
|
|
159
|
+
let end = i - 1;
|
|
160
|
+
while (end >= 0 && (code[end] !== 1 || /\s/.test(source[end] ?? "")))
|
|
161
|
+
end -= 1;
|
|
162
|
+
let start = end;
|
|
163
|
+
while (start >= 0 && code[start] === 1 && /[A-Za-z]/.test(source[start] ?? ""))
|
|
164
|
+
start -= 1;
|
|
165
|
+
return CONTROL_CONDITION_KEYWORDS.has(source.slice(start + 1, end + 1));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
function startsRegexLiteral(source, code, slashIndex) {
|
|
171
|
+
let previous = slashIndex - 1;
|
|
172
|
+
while (previous >= 0 && (code[previous] !== 1 || /\s/.test(source[previous] ?? "")))
|
|
173
|
+
previous -= 1;
|
|
174
|
+
if (previous < 0)
|
|
175
|
+
return true;
|
|
176
|
+
const previousChar = source[previous] ?? "";
|
|
177
|
+
if (previousChar === ")" && closesControlCondition(source, code, previous))
|
|
178
|
+
return true;
|
|
179
|
+
if (previousChar === "}")
|
|
180
|
+
return true;
|
|
181
|
+
if (/[([{:;,=!?&|+*%^~<>-]/.test(previousChar))
|
|
182
|
+
return true;
|
|
183
|
+
if (!/[A-Za-z]/.test(previousChar))
|
|
184
|
+
return false;
|
|
185
|
+
let start = previous;
|
|
186
|
+
while (start > 0 && /[A-Za-z]/.test(source[start - 1] ?? ""))
|
|
187
|
+
start -= 1;
|
|
188
|
+
return REGEX_PREFIX_KEYWORDS.has(source.slice(start, previous + 1));
|
|
189
|
+
}
|
|
190
|
+
function executableCodeMask(source) {
|
|
191
|
+
const code = new Uint8Array(source.length);
|
|
192
|
+
let quote = "";
|
|
193
|
+
let escaped = false;
|
|
194
|
+
let lineComment = false;
|
|
195
|
+
let blockComment = false;
|
|
196
|
+
let regex = false;
|
|
197
|
+
let regexClass = false;
|
|
198
|
+
for (let i = 0; i < source.length; i++) {
|
|
199
|
+
const char = source[i] ?? "";
|
|
200
|
+
const next = source[i + 1] ?? "";
|
|
201
|
+
if (lineComment) {
|
|
202
|
+
if (char === "\n" || char === "\r") {
|
|
203
|
+
lineComment = false;
|
|
204
|
+
code[i] = 1;
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (regex) {
|
|
209
|
+
if (escaped)
|
|
210
|
+
escaped = false;
|
|
211
|
+
else if (char === "\\")
|
|
212
|
+
escaped = true;
|
|
213
|
+
else if (char === "[")
|
|
214
|
+
regexClass = true;
|
|
215
|
+
else if (char === "]")
|
|
216
|
+
regexClass = false;
|
|
217
|
+
else if (char === "/" && !regexClass)
|
|
218
|
+
regex = false;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (blockComment) {
|
|
222
|
+
if (char === "*" && next === "/") {
|
|
223
|
+
blockComment = false;
|
|
224
|
+
i += 1;
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (quote) {
|
|
229
|
+
if (escaped)
|
|
230
|
+
escaped = false;
|
|
231
|
+
else if (char === "\\")
|
|
232
|
+
escaped = true;
|
|
233
|
+
else if (char === quote)
|
|
234
|
+
quote = "";
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (char === "/" && next === "/") {
|
|
238
|
+
lineComment = true;
|
|
239
|
+
i += 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (char === "/" && next === "*") {
|
|
243
|
+
blockComment = true;
|
|
244
|
+
i += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (source.startsWith("<!--", i)) {
|
|
248
|
+
lineComment = true;
|
|
249
|
+
i += 3;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (source.startsWith("-->", i) && /^\s*$/.test(source.slice(source.lastIndexOf("\n", i - 1) + 1, i))) {
|
|
253
|
+
lineComment = true;
|
|
254
|
+
i += 2;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (char === "/" && startsRegexLiteral(source, code, i)) {
|
|
258
|
+
regex = true;
|
|
259
|
+
regexClass = false;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
// Relay state uses JSON-compatible strings, never template literals. Mask
|
|
263
|
+
// the remainder rather than partially interpreting nested `${...}` code.
|
|
264
|
+
if (char === "`")
|
|
265
|
+
break;
|
|
266
|
+
if (char === '"' || char === "'") {
|
|
267
|
+
quote = char;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
code[i] = 1;
|
|
271
|
+
}
|
|
272
|
+
return code;
|
|
273
|
+
}
|
|
274
|
+
function serializedScriptPayload(html) {
|
|
275
|
+
let root;
|
|
276
|
+
try {
|
|
277
|
+
root = parse(html, { comment: false });
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const scripts = [];
|
|
283
|
+
for (const script of root.querySelectorAll("script")) {
|
|
284
|
+
if (script.hasAttribute("src") || script.hasAttribute("nomodule"))
|
|
285
|
+
continue;
|
|
286
|
+
const scriptType = (script.getAttribute("type") ?? "").trim().toLowerCase();
|
|
287
|
+
if (scriptType && !EXECUTABLE_SCRIPT_TYPES.has(scriptType))
|
|
288
|
+
continue;
|
|
289
|
+
const scriptLanguage = (script.getAttribute("language") ?? "").trim().toLowerCase();
|
|
290
|
+
if (!scriptType && scriptLanguage && !EXECUTABLE_SCRIPT_LANGUAGES.has(scriptLanguage))
|
|
291
|
+
continue;
|
|
292
|
+
let parent = script.parentNode;
|
|
293
|
+
let inert = false;
|
|
294
|
+
while (parent && "tagName" in parent) {
|
|
295
|
+
const tagName = typeof parent.tagName === "string" ? parent.tagName.toLowerCase() : "";
|
|
296
|
+
if ([
|
|
297
|
+
"iframe",
|
|
298
|
+
"math",
|
|
299
|
+
"noembed",
|
|
300
|
+
"noframes",
|
|
301
|
+
"noscript",
|
|
302
|
+
"plaintext",
|
|
303
|
+
"style",
|
|
304
|
+
"svg",
|
|
305
|
+
"template",
|
|
306
|
+
"textarea",
|
|
307
|
+
"title",
|
|
308
|
+
"xmp",
|
|
309
|
+
].includes(tagName)) {
|
|
310
|
+
inert = true;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
parent = parent.parentNode;
|
|
314
|
+
}
|
|
315
|
+
if (inert)
|
|
316
|
+
continue;
|
|
317
|
+
const body = script.innerHTML;
|
|
318
|
+
if (body.includes("$R[") || body.includes('__typename:"ArticleEntity"')) {
|
|
319
|
+
scripts.push({ source: body, code: executableCodeMask(body) });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (scripts.length === 0)
|
|
323
|
+
return null;
|
|
324
|
+
const separator = "\0;\n";
|
|
325
|
+
const source = scripts.map((script) => script.source).join(separator);
|
|
326
|
+
const code = new Uint8Array(source.length);
|
|
327
|
+
let offset = 0;
|
|
328
|
+
for (const [index, script] of scripts.entries()) {
|
|
329
|
+
code.set(script.code, offset);
|
|
330
|
+
offset += script.source.length;
|
|
331
|
+
if (index < scripts.length - 1) {
|
|
332
|
+
code.fill(1, offset, offset + separator.length);
|
|
333
|
+
offset += separator.length;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { source, code };
|
|
337
|
+
}
|
|
338
|
+
async function xApiJson(url, token, context) {
|
|
339
|
+
return (await fetchPinnedJson(url, {
|
|
340
|
+
headers: {
|
|
341
|
+
Accept: "application/json",
|
|
342
|
+
Authorization: `Bearer ${token}`,
|
|
343
|
+
"User-Agent": "akm-cli x fetcher",
|
|
344
|
+
},
|
|
345
|
+
byteCap: X_BYTE_CAP,
|
|
346
|
+
bodyTimeoutMs: X_BODY_TIMEOUT_MS,
|
|
347
|
+
timeoutMs: context.timeoutMs,
|
|
348
|
+
signal: context.signal,
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
async function fetchProfileViaApi(username, token, context) {
|
|
352
|
+
const lookup = await xApiJson(`${X_API_BASE}/users/by/username/${encodeURIComponent(username)}`, token, context);
|
|
353
|
+
const userId = coerceString(lookup?.data?.id);
|
|
354
|
+
if (!userId)
|
|
355
|
+
return null;
|
|
356
|
+
const timeline = await xApiJson(`${X_API_BASE}/users/${encodeURIComponent(userId)}/tweets` +
|
|
357
|
+
`?max_results=${DEFAULT_TWEET_LIMIT}&tweet.fields=created_at`, token, context);
|
|
358
|
+
const data = timeline?.data;
|
|
359
|
+
if (!Array.isArray(data))
|
|
360
|
+
return null;
|
|
361
|
+
return data.map((raw) => {
|
|
362
|
+
const tweet = (raw ?? {});
|
|
363
|
+
return {
|
|
364
|
+
id: validXId(coerceString(tweet.id)) ?? "",
|
|
365
|
+
text: coerceString(tweet.text),
|
|
366
|
+
createdAt: toIsoDate(coerceString(tweet.created_at)),
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async function fetchPostViaApi(postId, usernameHint, token, context) {
|
|
371
|
+
const params = new URLSearchParams({
|
|
372
|
+
"tweet.fields": "author_id,created_at,note_tweet",
|
|
373
|
+
expansions: "author_id",
|
|
374
|
+
"user.fields": "username",
|
|
375
|
+
});
|
|
376
|
+
const response = await xApiJson(`${X_API_BASE}/tweets/${encodeURIComponent(postId)}?${params}`, token, context);
|
|
377
|
+
const data = response?.data;
|
|
378
|
+
if (!data)
|
|
379
|
+
return null;
|
|
380
|
+
if (coerceString(data.id) !== postId)
|
|
381
|
+
return null;
|
|
382
|
+
const noteTweet = data.note_tweet;
|
|
383
|
+
const text = coerceString(noteTweet?.text) || coerceString(data.text);
|
|
384
|
+
if (!text)
|
|
385
|
+
return null;
|
|
386
|
+
const authorId = coerceString(data.author_id);
|
|
387
|
+
const users = response?.includes?.users;
|
|
388
|
+
const author = Array.isArray(users)
|
|
389
|
+
? users.find((raw) => coerceString(raw?.id) === authorId)
|
|
390
|
+
: undefined;
|
|
391
|
+
const username = validXUsername(coerceString(author?.username)) || usernameHint || "";
|
|
392
|
+
return {
|
|
393
|
+
id: postId,
|
|
394
|
+
text,
|
|
395
|
+
createdAt: toIsoDate(coerceString(data.created_at)),
|
|
396
|
+
username,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function toIsoDate(value) {
|
|
400
|
+
if (!value)
|
|
401
|
+
return "";
|
|
402
|
+
const parsed = new Date(value);
|
|
403
|
+
return Number.isNaN(parsed.getTime()) ? "" : parsed.toISOString();
|
|
404
|
+
}
|
|
405
|
+
function renderProfileMarkdown(username, tweets) {
|
|
406
|
+
const sections = [];
|
|
407
|
+
for (const tweet of tweets) {
|
|
408
|
+
if (!tweet.text)
|
|
409
|
+
continue;
|
|
410
|
+
const when = tweet.createdAt ? new Date(tweet.createdAt) : null;
|
|
411
|
+
const iso = when && !Number.isNaN(when.getTime()) ? when.toISOString() : "";
|
|
412
|
+
sections.push(`## ${iso || "(undated)"}`, "", escapeXPlainText(tweet.text), "");
|
|
413
|
+
if (tweet.id)
|
|
414
|
+
sections.push(`https://x.com/${username}/status/${tweet.id}`, "");
|
|
415
|
+
}
|
|
416
|
+
return sections.join("\n").trimEnd();
|
|
417
|
+
}
|
|
418
|
+
function readJsonStringAt(value, quoteIndex) {
|
|
419
|
+
if (value[quoteIndex] !== '"')
|
|
420
|
+
return null;
|
|
421
|
+
let escaped = false;
|
|
422
|
+
for (let i = quoteIndex + 1; i < value.length; i++) {
|
|
423
|
+
const char = value[i];
|
|
424
|
+
if (escaped) {
|
|
425
|
+
escaped = false;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (char === "\\") {
|
|
429
|
+
escaped = true;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (char !== '"')
|
|
433
|
+
continue;
|
|
434
|
+
try {
|
|
435
|
+
const parsed = JSON.parse(value.slice(quoteIndex, i + 1));
|
|
436
|
+
return typeof parsed === "string" ? { value: parsed, end: i + 1 } : null;
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
function readSerializedObjectAt(payload, openIndex) {
|
|
445
|
+
if (payload.source[openIndex] !== "{" || payload.code[openIndex] !== 1)
|
|
446
|
+
return null;
|
|
447
|
+
let depth = 0;
|
|
448
|
+
for (let i = openIndex; i < payload.source.length; i++) {
|
|
449
|
+
if (payload.code[i] !== 1)
|
|
450
|
+
continue;
|
|
451
|
+
const char = payload.source[i] ?? "";
|
|
452
|
+
if (char === "\0")
|
|
453
|
+
return null;
|
|
454
|
+
if (char === "{")
|
|
455
|
+
depth += 1;
|
|
456
|
+
else if (char === "}") {
|
|
457
|
+
depth -= 1;
|
|
458
|
+
if (depth === 0) {
|
|
459
|
+
return {
|
|
460
|
+
start: openIndex,
|
|
461
|
+
end: i + 1,
|
|
462
|
+
source: payload.source.slice(openIndex, i + 1),
|
|
463
|
+
code: payload.code.slice(openIndex, i + 1),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
function isRelayAssignment(payload, assignmentIndex) {
|
|
471
|
+
if (payload.code[assignmentIndex] !== 1 || payload.code[assignmentIndex + 1] !== 1)
|
|
472
|
+
return false;
|
|
473
|
+
const prefixStart = Math.max(0, assignmentIndex - 40);
|
|
474
|
+
const match = /\$R\[\d+\]$/.exec(payload.source.slice(prefixStart, assignmentIndex));
|
|
475
|
+
if (!match)
|
|
476
|
+
return false;
|
|
477
|
+
let boundaryIndex = prefixStart + match.index - 1;
|
|
478
|
+
while (boundaryIndex >= 0 && (payload.code[boundaryIndex] !== 1 || /\s/.test(payload.source[boundaryIndex] ?? ""))) {
|
|
479
|
+
boundaryIndex -= 1;
|
|
480
|
+
}
|
|
481
|
+
return boundaryIndex < 0 || /[:,;=({[]/.test(payload.source[boundaryIndex] ?? "");
|
|
482
|
+
}
|
|
483
|
+
function objectContaining(payload, markerIndex) {
|
|
484
|
+
let assignmentIndex = payload.source.lastIndexOf("={", markerIndex);
|
|
485
|
+
while (assignmentIndex >= 0) {
|
|
486
|
+
if (isRelayAssignment(payload, assignmentIndex)) {
|
|
487
|
+
const object = readSerializedObjectAt(payload, assignmentIndex + 1);
|
|
488
|
+
if (object && object.end > markerIndex)
|
|
489
|
+
return object;
|
|
490
|
+
}
|
|
491
|
+
assignmentIndex = payload.source.lastIndexOf("={", assignmentIndex - 1);
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
function topLevelPropertyValueIndexes(object, property) {
|
|
496
|
+
const indexes = [];
|
|
497
|
+
let depth = 0;
|
|
498
|
+
for (let i = 0; i < object.source.length; i++) {
|
|
499
|
+
if (object.code[i] !== 1)
|
|
500
|
+
continue;
|
|
501
|
+
const char = object.source[i] ?? "";
|
|
502
|
+
if (char === "{") {
|
|
503
|
+
depth += 1;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (char === "}") {
|
|
507
|
+
depth -= 1;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (depth !== 1 || !object.source.startsWith(property, i))
|
|
511
|
+
continue;
|
|
512
|
+
let beforeIndex = i - 1;
|
|
513
|
+
while (beforeIndex >= 0 && (object.code[beforeIndex] !== 1 || /\s/.test(object.source[beforeIndex] ?? ""))) {
|
|
514
|
+
beforeIndex -= 1;
|
|
515
|
+
}
|
|
516
|
+
const before = object.source[beforeIndex];
|
|
517
|
+
if (before !== "{" && before !== ",")
|
|
518
|
+
continue;
|
|
519
|
+
let cursor = i + property.length;
|
|
520
|
+
while (/\s/.test(object.source[cursor] ?? ""))
|
|
521
|
+
cursor += 1;
|
|
522
|
+
if (object.source[cursor] !== ":" || object.code[cursor] !== 1)
|
|
523
|
+
continue;
|
|
524
|
+
indexes.push(cursor + 1);
|
|
525
|
+
}
|
|
526
|
+
return indexes;
|
|
527
|
+
}
|
|
528
|
+
function nextExecutableIndex(object, from) {
|
|
529
|
+
for (let i = from; i < object.source.length; i++) {
|
|
530
|
+
if (object.code[i] === 1 && !/\s/.test(object.source[i] ?? ""))
|
|
531
|
+
return i;
|
|
532
|
+
}
|
|
533
|
+
return -1;
|
|
534
|
+
}
|
|
535
|
+
function topLevelStringProperty(object, property) {
|
|
536
|
+
let valueIndex = topLevelPropertyValueIndexes(object, property).at(-1) ?? -1;
|
|
537
|
+
if (valueIndex < 0)
|
|
538
|
+
return "";
|
|
539
|
+
while (/\s/.test(object.source[valueIndex] ?? ""))
|
|
540
|
+
valueIndex += 1;
|
|
541
|
+
const parsed = readJsonStringAt(object.source, valueIndex);
|
|
542
|
+
if (!parsed)
|
|
543
|
+
return "";
|
|
544
|
+
const delimiterIndex = nextExecutableIndex(object, parsed.end);
|
|
545
|
+
if (delimiterIndex < 0 || (object.source[delimiterIndex] !== "," && object.source[delimiterIndex] !== "}"))
|
|
546
|
+
return "";
|
|
547
|
+
return parsed.value;
|
|
548
|
+
}
|
|
549
|
+
function topLevelObjectProperty(object, property) {
|
|
550
|
+
const valueIndex = topLevelPropertyValueIndexes(object, property).at(-1) ?? -1;
|
|
551
|
+
if (valueIndex < 0)
|
|
552
|
+
return null;
|
|
553
|
+
let openIndex = -1;
|
|
554
|
+
for (let i = valueIndex; i < object.source.length; i++) {
|
|
555
|
+
if (object.code[i] !== 1)
|
|
556
|
+
continue;
|
|
557
|
+
if (object.source[i] === ",")
|
|
558
|
+
return null;
|
|
559
|
+
if (object.source[i] === "{") {
|
|
560
|
+
openIndex = i;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (openIndex < 0)
|
|
565
|
+
return null;
|
|
566
|
+
return readSerializedObjectAt(object, openIndex);
|
|
567
|
+
}
|
|
568
|
+
function findObjectById(payload, id) {
|
|
569
|
+
if (!/^[A-Za-z0-9_:+/=-]{1,512}$/.test(id))
|
|
570
|
+
return null;
|
|
571
|
+
const marker = `__id:"${id}"`;
|
|
572
|
+
let from = 0;
|
|
573
|
+
let found = null;
|
|
574
|
+
let candidates = 0;
|
|
575
|
+
while (from < payload.source.length) {
|
|
576
|
+
const index = payload.source.indexOf(marker, from);
|
|
577
|
+
if (index < 0)
|
|
578
|
+
break;
|
|
579
|
+
if (payload.code[index] !== 1) {
|
|
580
|
+
from = index + marker.length;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const object = objectContaining(payload, index);
|
|
584
|
+
if (object) {
|
|
585
|
+
candidates += 1;
|
|
586
|
+
if (candidates > MAX_RELAY_OBJECT_CANDIDATES)
|
|
587
|
+
return null;
|
|
588
|
+
if (topLevelStringProperty(object, "__id") === id) {
|
|
589
|
+
if (found && found.start !== object.start)
|
|
590
|
+
return null;
|
|
591
|
+
found = object;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
from = index + marker.length;
|
|
595
|
+
}
|
|
596
|
+
return found;
|
|
597
|
+
}
|
|
598
|
+
function articleFromObject(object) {
|
|
599
|
+
if (topLevelStringProperty(object, "__typename") !== "ArticleEntity")
|
|
600
|
+
return null;
|
|
601
|
+
const body = topLevelStringProperty(object, "plain_text").trim();
|
|
602
|
+
if (!body)
|
|
603
|
+
return null;
|
|
604
|
+
return {
|
|
605
|
+
title: topLevelStringProperty(object, "title").replace(/\s+/g, " ").trim(),
|
|
606
|
+
body,
|
|
607
|
+
restId: topLevelStringProperty(object, "rest_id"),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function referencedObject(payload, object, property) {
|
|
611
|
+
const reference = topLevelObjectProperty(object, property);
|
|
612
|
+
const id = reference ? topLevelStringProperty(reference, "__ref") : "";
|
|
613
|
+
return id ? findObjectById(payload, id) : null;
|
|
614
|
+
}
|
|
615
|
+
function serializedArticles(payload) {
|
|
616
|
+
const marker = '__typename:"ArticleEntity"';
|
|
617
|
+
const seen = new Set();
|
|
618
|
+
const articles = [];
|
|
619
|
+
let from = 0;
|
|
620
|
+
let candidates = 0;
|
|
621
|
+
while (from < payload.source.length) {
|
|
622
|
+
const index = payload.source.indexOf(marker, from);
|
|
623
|
+
if (index < 0)
|
|
624
|
+
break;
|
|
625
|
+
if (payload.code[index] !== 1) {
|
|
626
|
+
from = index + marker.length;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const object = objectContaining(payload, index);
|
|
630
|
+
if (object && !seen.has(object.start)) {
|
|
631
|
+
candidates += 1;
|
|
632
|
+
if (candidates > MAX_RELAY_OBJECT_CANDIDATES)
|
|
633
|
+
return [];
|
|
634
|
+
seen.add(object.start);
|
|
635
|
+
const article = articleFromObject(object);
|
|
636
|
+
if (article)
|
|
637
|
+
articles.push(article);
|
|
638
|
+
}
|
|
639
|
+
from = index + marker.length;
|
|
640
|
+
}
|
|
641
|
+
return articles;
|
|
642
|
+
}
|
|
643
|
+
function articleByRestId(payload, articleId) {
|
|
644
|
+
const marker = `rest_id:"${articleId}"`;
|
|
645
|
+
let from = 0;
|
|
646
|
+
let found = null;
|
|
647
|
+
let candidates = 0;
|
|
648
|
+
while (from < payload.source.length) {
|
|
649
|
+
const index = payload.source.indexOf(marker, from);
|
|
650
|
+
if (index < 0)
|
|
651
|
+
break;
|
|
652
|
+
if (payload.code[index] !== 1) {
|
|
653
|
+
from = index + marker.length;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const object = objectContaining(payload, index);
|
|
657
|
+
if (object) {
|
|
658
|
+
candidates += 1;
|
|
659
|
+
if (candidates > MAX_RELAY_OBJECT_CANDIDATES)
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
const article = object ? articleFromObject(object) : null;
|
|
663
|
+
if (article?.restId === articleId) {
|
|
664
|
+
if (found)
|
|
665
|
+
return null;
|
|
666
|
+
found = article;
|
|
667
|
+
}
|
|
668
|
+
from = index + marker.length;
|
|
669
|
+
}
|
|
670
|
+
return found;
|
|
671
|
+
}
|
|
672
|
+
export function extractPublicXArticle(html, target) {
|
|
673
|
+
const payload = serializedScriptPayload(html);
|
|
674
|
+
if (!payload)
|
|
675
|
+
return null;
|
|
676
|
+
let article;
|
|
677
|
+
if (target?.articleId) {
|
|
678
|
+
article = articleByRestId(payload, target.articleId) ?? undefined;
|
|
679
|
+
}
|
|
680
|
+
else if (target?.postId) {
|
|
681
|
+
const encodedId = Buffer.from(`Tweet:${target.postId}`).toString("base64");
|
|
682
|
+
const tweetArticle = findObjectById(payload, `client:${encodedId}:article`);
|
|
683
|
+
const articleResults = tweetArticle ? referencedObject(payload, tweetArticle, "article_results") : null;
|
|
684
|
+
const articleObject = articleResults ? referencedObject(payload, articleResults, "result") : null;
|
|
685
|
+
article = articleObject ? (articleFromObject(articleObject) ?? undefined) : undefined;
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const articles = serializedArticles(payload);
|
|
689
|
+
if (articles.length === 1)
|
|
690
|
+
article = articles[0];
|
|
691
|
+
}
|
|
692
|
+
return article ? { title: article.title, body: article.body } : null;
|
|
693
|
+
}
|
|
694
|
+
function escapeXPlainText(value) {
|
|
695
|
+
const inlineSafe = value
|
|
696
|
+
.replace(/\r\n?/g, "\n")
|
|
697
|
+
.replace(/\\/g, "\\\\")
|
|
698
|
+
.replace(/([[\]`])/g, "\\$1")
|
|
699
|
+
.replace(/<(?=[a-zA-Z/!?])/g, "<");
|
|
700
|
+
return escapeMarkdownStructure(inlineSafe);
|
|
701
|
+
}
|
|
702
|
+
function metaContent(root, name) {
|
|
703
|
+
for (const meta of root.querySelectorAll("meta")) {
|
|
704
|
+
const key = (meta.getAttribute("property") ?? meta.getAttribute("name") ?? "").toLowerCase();
|
|
705
|
+
if (key === name)
|
|
706
|
+
return meta.getAttribute("content")?.trim() ?? "";
|
|
707
|
+
}
|
|
708
|
+
return "";
|
|
709
|
+
}
|
|
710
|
+
function targetFullText(html, postId) {
|
|
711
|
+
const payload = serializedScriptPayload(html);
|
|
712
|
+
if (!payload)
|
|
713
|
+
return "";
|
|
714
|
+
const encodedId = Buffer.from(`Tweet:${postId}`).toString("base64");
|
|
715
|
+
const details = findObjectById(payload, `client:${encodedId}:details`);
|
|
716
|
+
return details ? topLevelStringProperty(details, "full_text") : "";
|
|
717
|
+
}
|
|
718
|
+
function publicPostFromHtml(html, postId, usernameHint) {
|
|
719
|
+
let root;
|
|
720
|
+
try {
|
|
721
|
+
root = parse(html, { comment: false });
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const title = metaContent(root, "og:title");
|
|
727
|
+
const titleUsername = title.match(/\(@([A-Za-z0-9_]{1,15})\)/)?.[1] ?? "";
|
|
728
|
+
const text = (titleUsername ? metaContent(root, "og:description") : "") || targetFullText(html, postId);
|
|
729
|
+
if (!text.trim())
|
|
730
|
+
return null;
|
|
731
|
+
const username = titleUsername || usernameHint || "";
|
|
732
|
+
return { id: postId, text: text.trim(), createdAt: "", username };
|
|
733
|
+
}
|
|
734
|
+
async function fetchPublicXHtml(url, context) {
|
|
735
|
+
try {
|
|
736
|
+
const fetched = await fetchGuardedResponse(url.toString(), {
|
|
737
|
+
headers: { Accept: "text/html, application/xhtml+xml", "User-Agent": "akm-cli x fetcher" },
|
|
738
|
+
signal: context.signal,
|
|
739
|
+
}, { timeoutMs: context.timeoutMs, retries: 1, allowPrivateHosts: context.allowPrivateHosts });
|
|
740
|
+
const { response } = fetched;
|
|
741
|
+
if (!response.ok) {
|
|
742
|
+
await response.body?.cancel().catch(() => undefined);
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
const html = await readBodyWithByteCap(response, X_PAGE_BYTE_CAP, {
|
|
746
|
+
bodyTimeoutMs: X_BODY_TIMEOUT_MS,
|
|
747
|
+
signal: context.signal,
|
|
748
|
+
});
|
|
749
|
+
return { html, finalUrl: fetched.finalUrl };
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
if (context.signal?.aborted)
|
|
753
|
+
throw error;
|
|
754
|
+
if (error instanceof ResponseTooLargeError)
|
|
755
|
+
return null;
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function postSnapshot(post) {
|
|
760
|
+
const postId = validXId(post.id);
|
|
761
|
+
if (!postId)
|
|
762
|
+
throw new Error("X API returned an invalid post id");
|
|
763
|
+
const username = post.username || "unknown";
|
|
764
|
+
const canonicalUrl = post.username
|
|
765
|
+
? `https://x.com/${post.username}/status/${postId}`
|
|
766
|
+
: `https://x.com/i/status/${postId}`;
|
|
767
|
+
const markdown = [escapeXPlainText(post.text), post.createdAt ? `\n${post.createdAt}` : ""]
|
|
768
|
+
.filter(Boolean)
|
|
769
|
+
.join("\n");
|
|
770
|
+
return {
|
|
771
|
+
url: canonicalUrl,
|
|
772
|
+
title: post.username ? `X post by @${post.username}` : `X post ${postId}`,
|
|
773
|
+
markdown,
|
|
774
|
+
preferredName: avoidReservedBasename(`x/${username}/status/${postId}`),
|
|
775
|
+
tags: ["x", "twitter", "social", "post"],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function articleSnapshot(article, resource, usernameHint) {
|
|
779
|
+
const id = resource.kind === "article" ? resource.articleId : resource.postId;
|
|
780
|
+
const url = resource.kind === "article"
|
|
781
|
+
? `https://x.com/i/article/${resource.articleId}`
|
|
782
|
+
: usernameHint
|
|
783
|
+
? `https://x.com/${usernameHint}/status/${resource.postId}`
|
|
784
|
+
: `https://x.com/i/status/${resource.postId}`;
|
|
785
|
+
const preferredName = resource.kind === "article"
|
|
786
|
+
? `x/article/${resource.articleId}`
|
|
787
|
+
: usernameHint
|
|
788
|
+
? `x/${usernameHint}/status/${resource.postId}`
|
|
789
|
+
: `x/status/${resource.postId}`;
|
|
790
|
+
return {
|
|
791
|
+
url,
|
|
792
|
+
title: article.title || `X Article ${id}`,
|
|
793
|
+
markdown: escapeXPlainText(article.body),
|
|
794
|
+
preferredName: avoidReservedBasename(preferredName),
|
|
795
|
+
tags: ["x", "twitter", "social", "article"],
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/** Build the RSS fallback URL from a template containing `{username}`. */
|
|
799
|
+
export function buildXRssUrl(template, username) {
|
|
800
|
+
const raw = template?.trim();
|
|
801
|
+
if (!raw)
|
|
802
|
+
return null;
|
|
803
|
+
if (!raw.includes("{username}")) {
|
|
804
|
+
warn('[akm] x-profile: X_RSS_TEMPLATE must contain the "{username}" placeholder; ignoring it.');
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
const url = new URL(raw.replaceAll("{username}", encodeURIComponent(username)));
|
|
809
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
810
|
+
return null;
|
|
811
|
+
return url;
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async function fetchProfile(username, context) {
|
|
818
|
+
const token = resolveXBearerToken(context);
|
|
819
|
+
if (token) {
|
|
820
|
+
try {
|
|
821
|
+
const tweets = await fetchProfileViaApi(username, token, context);
|
|
822
|
+
const markdown = tweets ? renderProfileMarkdown(username, tweets) : "";
|
|
823
|
+
if (markdown) {
|
|
824
|
+
return {
|
|
825
|
+
url: `https://x.com/${username}`,
|
|
826
|
+
title: `X — @${username}`,
|
|
827
|
+
markdown,
|
|
828
|
+
preferredName: avoidReservedBasename(`x/${username}`),
|
|
829
|
+
tags: ["x", "twitter", "social"],
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
if (context.signal?.aborted)
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const rssUrl = buildXRssUrl(process.env.X_RSS_TEMPLATE, username);
|
|
839
|
+
if (rssUrl) {
|
|
840
|
+
const snapshot = await rssFetcher.fetch(rssUrl, context);
|
|
841
|
+
if (snapshot) {
|
|
842
|
+
return {
|
|
843
|
+
...snapshot,
|
|
844
|
+
url: `https://x.com/${username}`,
|
|
845
|
+
title: `X — @${username}`,
|
|
846
|
+
preferredName: avoidReservedBasename(`x/${username}`),
|
|
847
|
+
tags: ["x", "twitter", "social"],
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
warn("[akm] x-profile: no content for @%s. Set X_BEARER_TOKEN, or store the token as the " +
|
|
852
|
+
"secrets/x-bearer-token akm secret, or set X_RSS_TEMPLATE to an RSS bridge URL containing {username}.", username);
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
const xFetcher = {
|
|
856
|
+
name: "x",
|
|
857
|
+
matches(url) {
|
|
858
|
+
return extractXResource(url) !== null;
|
|
859
|
+
},
|
|
860
|
+
async fetch(url, context) {
|
|
861
|
+
const resource = extractXResource(url);
|
|
862
|
+
if (!resource)
|
|
863
|
+
return null;
|
|
864
|
+
if (resource.kind === "profile")
|
|
865
|
+
return fetchProfile(resource.username, context);
|
|
866
|
+
const publicUrl = resource.kind === "status"
|
|
867
|
+
? new URL(resource.usernameHint
|
|
868
|
+
? `https://x.com/${resource.usernameHint}/status/${resource.postId}`
|
|
869
|
+
: `https://x.com/i/status/${resource.postId}`)
|
|
870
|
+
: url;
|
|
871
|
+
const publicPage = await fetchPublicXHtml(publicUrl, context);
|
|
872
|
+
let html = null;
|
|
873
|
+
let resolvedResource = resource;
|
|
874
|
+
if (publicPage) {
|
|
875
|
+
const finalResource = extractXResource(new URL(publicPage.finalUrl));
|
|
876
|
+
if ((resource.kind === "status" && finalResource?.kind === "status" && finalResource.postId === resource.postId) ||
|
|
877
|
+
(resource.kind === "article" &&
|
|
878
|
+
finalResource?.kind === "article" &&
|
|
879
|
+
finalResource.articleId === resource.articleId)) {
|
|
880
|
+
resolvedResource = finalResource;
|
|
881
|
+
html = publicPage.html;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (html) {
|
|
885
|
+
const article = extractPublicXArticle(html, resource.kind === "status" ? { postId: resource.postId } : { articleId: resource.articleId });
|
|
886
|
+
if (article) {
|
|
887
|
+
return articleSnapshot(article, resolvedResource, resolvedResource.kind === "status" ? resolvedResource.usernameHint : undefined);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (resource.kind === "article")
|
|
891
|
+
return null;
|
|
892
|
+
const token = resolveXBearerToken(context);
|
|
893
|
+
if (token) {
|
|
894
|
+
try {
|
|
895
|
+
const post = await fetchPostViaApi(resource.postId, resolvedResource.kind === "status" ? resolvedResource.usernameHint : resource.usernameHint, token, context);
|
|
896
|
+
if (post)
|
|
897
|
+
return postSnapshot(post);
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
if (context.signal?.aborted)
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const publicPost = html
|
|
905
|
+
? publicPostFromHtml(html, resource.postId, resolvedResource.kind === "status" ? resolvedResource.usernameHint : resource.usernameHint)
|
|
906
|
+
: null;
|
|
907
|
+
return publicPost ? postSnapshot(publicPost) : null;
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
export default xFetcher;
|