@yemi33/minions 0.1.2448 → 0.1.2449
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/bin/cli-api-client.js +1 -1
- package/bin/install-internal-minions.js +1382 -44
- package/bin/install-layout.js +150 -0
- package/bin/minions.js +460 -167
- package/dashboard/docs/typography.md +65 -12
- package/dashboard/js/command-center.js +58 -4
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +179 -20
- package/dashboard/js/refresh.js +148 -12
- package/dashboard/js/render-dispatch.js +3 -4
- package/dashboard/js/render-inbox.js +2 -2
- package/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-pipelines.js +14 -0
- package/dashboard/js/render-plans.js +57 -9
- package/dashboard/js/render-prd.js +132 -23
- package/dashboard/js/render-prs.js +195 -166
- package/dashboard/js/render-schedules.js +63 -3
- package/dashboard/js/render-utils.js +3 -3
- package/dashboard/js/render-watches.js +19 -3
- package/dashboard/js/render-work-items.js +238 -30
- package/dashboard/js/settings.js +205 -54
- package/dashboard/js/utils.js +51 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/qa.html +1 -16
- package/dashboard/pages/work.html +40 -0
- package/dashboard/shared/cc-limits.js +79 -0
- package/dashboard/shared/pr-filters.js +21 -38
- package/dashboard/shared/project-git-summary.js +1 -1
- package/dashboard/shared/record-filters.js +169 -0
- package/dashboard/shared/watches-source.js +1 -1
- package/dashboard/shared/welcome-popup.js +1 -1
- package/dashboard/shared/wi-filters.js +302 -0
- package/dashboard/slim/body.html +1 -0
- package/dashboard/slim/js/command-send.js +26 -0
- package/dashboard/slim/js/modals-tiles.js +380 -39
- package/dashboard/slim/js/status.js +13 -21
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/panel-bootstrap.js +6 -2
- package/dashboard/slim/styles.css +38 -0
- package/dashboard/styles.css +159 -55
- package/dashboard-build.js +13 -2
- package/dashboard.js +956 -423
- package/docs/README.md +11 -6
- package/docs/api-errors.md +2 -2
- package/docs/architecture-review-2026-07-09.md +1 -1
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +18 -9
- package/docs/branch-derivation.md +4 -4
- package/docs/capture-demos.js +39 -2
- package/docs/ci-runner-canary.md +123 -0
- package/docs/claude-md-propagation.md +2 -2
- package/docs/cloud-agent-dispatch.md +204 -0
- package/docs/command-center.md +7 -7
- package/docs/completion-reports.md +43 -20
- package/docs/constants.md +10 -3
- package/docs/constellation-bridge.md +134 -6
- package/docs/constellation-style-telemetry.md +4 -4
- package/docs/contracts/capability-protocol.v1.json +165 -0
- package/docs/cooldown-merge-semantics.md +12 -12
- package/docs/copilot-cli-schema.md +7 -7
- package/docs/cross-repo-plans.md +10 -10
- package/docs/dead-code-audit-retractions.md +5 -5
- package/docs/default-branch-ci.md +173 -0
- package/docs/deprecated.json +31 -31
- package/docs/design-inbox-entries-schema.md +3 -3
- package/docs/design-language.md +1051 -0
- package/docs/design-state-storage.md +11 -11
- package/docs/diagnostics-crash-reports.md +9 -9
- package/docs/diagnostics-memory.md +5 -5
- package/docs/documentation-audit-2026-07-09.md +7 -7
- package/docs/engine-restart.md +90 -5
- package/docs/harness-mode.md +1 -1
- package/docs/internal-install.md +338 -39
- package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
- package/docs/kb-pr3223-cascade-archiving.md +1 -1
- package/docs/kb-pr696-merge-conflict-docs.md +6 -6
- package/docs/kb-sweep.md +35 -35
- package/docs/keep-processes.md +1 -1
- package/docs/live-checkout-mode.md +30 -30
- package/docs/managed-spawn.md +18 -14
- package/docs/named-agents.md +7 -7
- package/docs/plan-lifecycle.md +69 -2
- package/docs/pr-author-identity.md +114 -0
- package/docs/pr-auto-fix-dispatch.md +19 -4
- package/docs/pr-comment-followup.md +6 -6
- package/docs/pr-review-fix-loop.md +59 -10
- package/docs/process-termination.md +40 -0
- package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
- package/docs/qa-runbook-lifecycle.md +367 -17
- package/docs/qa-runbooks.md +3 -3
- package/docs/rfc-completion-json.md +18 -18
- package/docs/runtime-adapters.md +26 -21
- package/docs/security.md +6 -6
- package/docs/self-improvement.md +4 -4
- package/docs/shared-lifecycle-module-map.md +473 -472
- package/docs/skills.md +52 -3
- package/docs/slim-ux/concepts.md +121 -116
- package/docs/specs/agent-configurability.md +18 -18
- package/docs/specs/agent-rename.md +18 -18
- package/docs/team-memory.md +38 -21
- package/docs/timeouts-and-liveness.md +118 -10
- package/docs/tutorials/01-install-and-connect.md +1 -1
- package/docs/watches.md +40 -39
- package/docs/workspace-manifests.md +4 -4
- package/docs/worktree-lifecycle.md +293 -14
- package/engine/README.md +46 -0
- package/engine/{ado-comment.js → ado/comment.js} +8 -8
- package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
- package/engine/{ado.js → ado/index.js} +417 -63
- package/engine/{ado-status.js → ado/status.js} +6 -8
- package/engine/{ado-token.js → ado/token.js} +1 -1
- package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
- package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
- package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
- package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
- package/engine/{harness-context.js → agents/harness-context.js} +5 -5
- package/engine/{harness.js → agents/harness.js} +3 -3
- package/engine/{llm.js → agents/llm.js} +18 -14
- package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
- package/engine/{playbook.js → agents/playbook.js} +155 -22
- package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
- package/engine/{preflight.js → agents/preflight.js} +29 -10
- package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
- package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
- package/engine/{steering.js → agents/steering.js} +5 -5
- package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
- package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
- package/engine/{api-validation.js → api/api-validation.js} +1 -1
- package/engine/api/bridge.js +787 -0
- package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
- package/engine/api/companion.js +560 -0
- package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
- package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
- package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
- package/engine/api-contracts/agent-content.js +4 -4
- package/engine/api-contracts/capability-manifest.js +236 -0
- package/engine/api-contracts/capability-protocol.js +333 -0
- package/engine/api-contracts/cc-ops.js +1 -1
- package/engine/api-contracts/config-runtime.js +5 -0
- package/engine/api-contracts/core.js +28 -1
- package/engine/api-contracts/index.js +100 -0
- package/engine/api-contracts/orchestration.js +18 -5
- package/engine/api-contracts/pull-requests.js +37 -6
- package/engine/api-contracts/qa-process.js +29 -6
- package/engine/api-contracts/work-plan-prd.js +21 -1
- package/engine/cloud/contract.js +212 -0
- package/engine/cloud/index.js +159 -0
- package/engine/{execution-model.js → core/execution-model.js} +1 -1
- package/engine/{features.js → core/features.js} +4 -4
- package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
- package/engine/{queries.js → core/queries.js} +201 -36
- package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
- package/engine/{shared.js → core/shared.js} +1637 -175
- package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
- package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
- package/engine/db/index.js +11 -2
- package/engine/db/migrations/002-dispatches.js +3 -3
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/db/migrations/006-metrics.js +1 -1
- package/engine/db/migrations/007-watches.js +2 -2
- package/engine/db/migrations/008-small-state.js +1 -1
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/010-pr-links.js +1 -1
- package/engine/db/migrations/011-remaining-state.js +1 -1
- package/engine/db/migrations/012-steering-deliveries.js +2 -2
- package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
- package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
- package/engine/db/migrations/015-plans-prds.js +0 -0
- package/engine/db/migrations/018-sql-only-cutover.js +2 -2
- package/engine/db/migrations/021-archived-work-items.js +1 -1
- package/engine/db/migrations/022-global-cc-session.js +1 -1
- package/engine/db/migrations/023-engine-state.js +1 -1
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
- package/engine/db/migrations/029-repair-reused-versions.js +20 -0
- package/engine/db/migrations/031-pr-author-identity.js +137 -0
- package/engine/{consolidation.js → memory/consolidation.js} +6 -6
- package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
- package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
- package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
- package/engine/{memory-store.js → memory/memory-store.js} +3 -3
- package/engine/{promotion.js → memory/promotion.js} +3 -3
- package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
- package/engine/{review-learning.js → memory/review-learning.js} +10 -5
- package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
- package/engine/{logs-store.js → observability/logs-store.js} +5 -5
- package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
- package/engine/{check-status.js → operations/check-status.js} +3 -3
- package/engine/{cli.js → operations/cli.js} +271 -113
- package/engine/{distribution.js → operations/distribution.js} +5 -6
- package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
- package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
- package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
- package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
- package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
- package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
- package/engine/{meeting.js → orchestration/meeting.js} +6 -16
- package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
- package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
- package/engine/{routing.js → orchestration/routing.js} +3 -3
- package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
- package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
- package/engine/{timeout.js → orchestration/timeout.js} +158 -109
- package/engine/{db-events.js → persistence/db-events.js} +2 -2
- package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
- package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
- package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
- package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
- package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
- package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
- package/engine/persistence/state-operations.js +350 -0
- package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
- package/engine/{issues.js → planning/issues.js} +2 -2
- package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
- package/engine/planning/prd-result-sidecar.js +190 -0
- package/engine/{prd-store.js → planning/prd-store.js} +17 -17
- package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
- package/engine/{projects.js → planning/projects.js} +10 -10
- package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
- package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
- package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
- package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
- package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
- package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
- package/engine/{process-utils.js → processes/process-utils.js} +599 -55
- package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
- package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
- package/engine/{comment-format.js → providers/comment-format.js} +5 -5
- package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
- package/engine/{gh-token.js → providers/gh-token.js} +4 -4
- package/engine/{github.js → providers/github.js} +131 -54
- package/engine/{pr-action.js → providers/pr-action.js} +13 -12
- package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
- package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
- package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
- package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
- package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
- package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
- package/engine/{pr-track.js → providers/pr-track.js} +11 -13
- package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
- package/engine/qa/auto-prd-qa.js +313 -0
- package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
- package/engine/qa/prd-session.js +240 -0
- package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
- package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
- package/engine/{qa-runs.js → qa/runs.js} +286 -15
- package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
- package/engine/qa/visual-journey.js +654 -0
- package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
- package/engine/qa-runners/maestro.js +3 -3
- package/engine/qa-runners/playwright.js +2 -2
- package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
- package/engine/recovery/stop-stack.js +607 -0
- package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
- package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
- package/engine/runtimes/claude.js +14 -12
- package/engine/runtimes/codex.js +8 -6
- package/engine/runtimes/copilot.js +17 -16
- package/engine/{watch-actions.js → watches/actions.js} +13 -13
- package/engine/{watches.js → watches/index.js} +43 -32
- package/engine/{watches-store.js → watches/store.js} +4 -4
- package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
- package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
- package/engine/worktrees/inventory.js +671 -0
- package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
- package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
- package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
- package/engine/worktrees/quarantine-refs.js +173 -0
- package/engine.js +1137 -208
- package/minions.js +147 -77
- package/package.json +10 -6
- package/playbooks/_pr-description-audit.md +110 -78
- package/playbooks/build-fix-complex.md +2 -0
- package/playbooks/fix.md +16 -12
- package/playbooks/implement-shared.md +2 -0
- package/playbooks/implement.md +19 -20
- package/playbooks/plan-to-prd.md +18 -3
- package/playbooks/qa-session-draft.md +136 -1
- package/playbooks/qa-session-execute.md +80 -2
- package/playbooks/qa-session-setup.md +17 -1
- package/playbooks/qa-validate.md +1 -1
- package/playbooks/setup.md +2 -0
- package/playbooks/shared-rules.md +25 -32
- package/playbooks/templates/followup-dispatch.md +4 -3
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +19 -27
- package/watch-plugins/README.md +92 -0
- package/watch-plugins/ado-author-prs.js +336 -0
- package/watch-plugins/gh-author-prs.js +375 -0
- package/watch-plugins/http.js +474 -0
- package/watch-plugins/teams-channel.js +869 -0
- package/docs/dev-composite-workflow.md +0 -101
- package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
- package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
- package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/visual-evidence-ci.md +0 -103
- package/engine/bridge.js +0 -379
- package/engine/quarantine-refs.js +0 -103
- package/engine/state-operations.js +0 -178
- /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watch-plugins/http.js — Generic HTTP target type (W-mp7i22mu00191b07)
|
|
3
|
+
*
|
|
4
|
+
* The canonical watch target plugin — demonstrates the extension contract
|
|
5
|
+
* landed in W-mp7hg58e000b5212. Unlocks "watch any REST endpoint / GH Actions
|
|
6
|
+
* workflow_run / Jenkins job / Slack thread" without writing more bespoke
|
|
7
|
+
* target types.
|
|
8
|
+
*
|
|
9
|
+
* ── Target syntax ────────────────────────────────────────────────────────────
|
|
10
|
+
* target: 'https://api.github.com/zen'
|
|
11
|
+
* → bare URL, GET, JSON parsed automatically when content-type is JSON.
|
|
12
|
+
*
|
|
13
|
+
* target: { url, method, headers, body, extract, expected, regex, timeoutMs }
|
|
14
|
+
* → full config. `extract` is a JSONPath-style accessor (`$.foo[0].bar`)
|
|
15
|
+
* or a `/regex/flags` string; the result becomes `state.extracted` and
|
|
16
|
+
* is what `value-*` conditions compare. `expected` (for value-equals)
|
|
17
|
+
* and `regex` (for value-matches) live alongside on the target.
|
|
18
|
+
*
|
|
19
|
+
* Auth: `headers.Authorization: 'Bearer {{env.GH_TOKEN}}'` substitutes from
|
|
20
|
+
* `process.env` at fetch time. Missing env var = WARN log + header dropped.
|
|
21
|
+
* Secrets are never persisted in watch records.
|
|
22
|
+
*
|
|
23
|
+
* ── Conditions ──────────────────────────────────────────────────────────────
|
|
24
|
+
* status-change — HTTP status code differs from previous poll
|
|
25
|
+
* value-change — `state.extracted` differs (deep equal)
|
|
26
|
+
* value-equals — `state.extracted === target.expected`
|
|
27
|
+
* value-matches — `state.extracted` matches `new RegExp(target.regex)`
|
|
28
|
+
* http-error — HTTP status >= 400 OR fetch threw
|
|
29
|
+
*
|
|
30
|
+
* All conditions are change-based — `stopAfter:0` truly runs forever
|
|
31
|
+
* (absoluteConditions: []).
|
|
32
|
+
*
|
|
33
|
+
* ── Templating vars exposed to actions ───────────────────────────────────────
|
|
34
|
+
* {{httpStatus}}, {{url}}, {{extracted}}, {{prevExtracted}},
|
|
35
|
+
* {{fetchedAt}}, {{durationMs}}, {{rawBody}} (truncated to 4KB).
|
|
36
|
+
* Plus standard watch vars (target, condition, watchId, message, ...).
|
|
37
|
+
*
|
|
38
|
+
* ── Fetch model ─────────────────────────────────────────────────────────────
|
|
39
|
+
* The engine's checkWatches loop is synchronous (it runs inside a
|
|
40
|
+
* mutateJsonFileLocked callback), so this plugin maintains its own background
|
|
41
|
+
* fetch cache. `fetchEntity` is sync — it returns the cached entity and, when
|
|
42
|
+
* the cached value is older than 30s, kicks off a background refresh. First
|
|
43
|
+
* tick after a watch is created therefore returns null (no entity yet);
|
|
44
|
+
* second tick captures the baseline; subsequent ticks detect changes.
|
|
45
|
+
*
|
|
46
|
+
* Tests can short-circuit this entirely by passing a pre-populated map at
|
|
47
|
+
* `state.httpResponses[targetKey]` (see `_targetKey`) — the plugin uses that
|
|
48
|
+
* shape directly, no async required. Tests can also override the network
|
|
49
|
+
* fetcher via `setFetcher(fn)` for unit-testing the fetch-and-extract path.
|
|
50
|
+
*
|
|
51
|
+
* Out of scope: webhooks, auth schemes beyond `headers`, streaming / chunked
|
|
52
|
+
* downloads.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
const http = require('http');
|
|
56
|
+
const https = require('https');
|
|
57
|
+
const { URL } = require('url');
|
|
58
|
+
|
|
59
|
+
// Plugins live in `<MINIONS_DIR>/watch-plugins/` which may not be a sibling of
|
|
60
|
+
// the engine source on installed layouts, so this plugin avoids requiring
|
|
61
|
+
// `engine/core/shared.js`. The two helpers it needs (timestamp + warn-logger) are
|
|
62
|
+
// inlined: `ts()` matches shared.ts(), and `_warn()` falls back to
|
|
63
|
+
// `console.warn` (no dependency on the structured-log file).
|
|
64
|
+
function ts() { return new Date().toISOString(); }
|
|
65
|
+
function _warn(msg) { try { console.warn(`[${ts()}] [warn] ${msg}`); } catch {} }
|
|
66
|
+
|
|
67
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
68
|
+
const MAX_BODY_BYTES = 4096;
|
|
69
|
+
const CACHE_FRESHNESS_MS = 30000;
|
|
70
|
+
|
|
71
|
+
// Module-level fetch cache. fetchEntity returns from here synchronously while
|
|
72
|
+
// background refreshes update it as needed. Keyed by _targetKey(target).
|
|
73
|
+
const _cache = new Map();
|
|
74
|
+
const _inFlight = new Set();
|
|
75
|
+
|
|
76
|
+
// Override-able fetcher — default uses node:http(s). Tests inject a mock.
|
|
77
|
+
let _fetcher = _defaultHttpFetch;
|
|
78
|
+
function setFetcher(fn) { _fetcher = typeof fn === 'function' ? fn : _defaultHttpFetch; }
|
|
79
|
+
function _resetCache() { _cache.clear(); _inFlight.clear(); }
|
|
80
|
+
|
|
81
|
+
function _targetKey(target) {
|
|
82
|
+
if (typeof target === 'string') return target;
|
|
83
|
+
// Stable key — ordered serialization via Object.keys().sort() so cache
|
|
84
|
+
// hits don't depend on caller's property ordering.
|
|
85
|
+
if (target && typeof target === 'object') {
|
|
86
|
+
const ordered = {};
|
|
87
|
+
for (const k of Object.keys(target).sort()) ordered[k] = target[k];
|
|
88
|
+
return JSON.stringify(ordered);
|
|
89
|
+
}
|
|
90
|
+
return String(target);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _resolveCfg(target) {
|
|
94
|
+
if (typeof target === 'string' && target) {
|
|
95
|
+
return {
|
|
96
|
+
url: target, method: 'GET', headers: {}, body: null, extract: null,
|
|
97
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (target && typeof target === 'object' && typeof target.url === 'string' && target.url) {
|
|
101
|
+
return {
|
|
102
|
+
url: target.url,
|
|
103
|
+
method: String(target.method || 'GET').toUpperCase(),
|
|
104
|
+
headers: (target.headers && typeof target.headers === 'object') ? target.headers : {},
|
|
105
|
+
body: target.body ?? null,
|
|
106
|
+
extract: target.extract || null,
|
|
107
|
+
timeoutMs: Number.isFinite(Number(target.timeoutMs)) ? Number(target.timeoutMs) : DEFAULT_TIMEOUT_MS,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _validateTarget(target, context = {}) {
|
|
114
|
+
const cfg = _resolveCfg(target);
|
|
115
|
+
if (!cfg) return 'target must be an HTTP(S) URL string or an object with a url';
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = new URL(cfg.url);
|
|
119
|
+
} catch {
|
|
120
|
+
return 'target.url must be a valid HTTP(S) URL';
|
|
121
|
+
}
|
|
122
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
123
|
+
return 'target.url must use http: or https:';
|
|
124
|
+
}
|
|
125
|
+
const targetObject = target && typeof target === 'object' ? target : null;
|
|
126
|
+
if (targetObject) {
|
|
127
|
+
if (target.method !== undefined) {
|
|
128
|
+
if (typeof target.method !== 'string') return 'target.method must be a valid HTTP token';
|
|
129
|
+
try {
|
|
130
|
+
http.validateHeaderName(target.method);
|
|
131
|
+
} catch {
|
|
132
|
+
return 'target.method must be a valid HTTP token';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (target.headers !== undefined) {
|
|
136
|
+
if (!target.headers || typeof target.headers !== 'object' || Array.isArray(target.headers)) {
|
|
137
|
+
return 'target.headers must be an object';
|
|
138
|
+
}
|
|
139
|
+
for (const [name, value] of Object.entries(target.headers)) {
|
|
140
|
+
if (typeof value !== 'string') return 'target.headers values must be strings';
|
|
141
|
+
try {
|
|
142
|
+
http.validateHeaderName(name);
|
|
143
|
+
http.validateHeaderValue(name, value);
|
|
144
|
+
} catch {
|
|
145
|
+
return 'target.headers contains an invalid HTTP header';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (target.extract !== undefined && target.extract !== null
|
|
150
|
+
&& (typeof target.extract !== 'string' || !target.extract.trim())) {
|
|
151
|
+
return 'target.extract must be a non-empty string';
|
|
152
|
+
}
|
|
153
|
+
if (target.regex !== undefined && target.regex !== null) {
|
|
154
|
+
if (typeof target.regex !== 'string' || !target.regex) {
|
|
155
|
+
return 'target.regex must be a non-empty string';
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
new RegExp(target.regex);
|
|
159
|
+
} catch {
|
|
160
|
+
return 'target.regex must be a valid regular expression';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (target.timeoutMs !== undefined
|
|
164
|
+
&& (typeof target.timeoutMs !== 'number'
|
|
165
|
+
|| !Number.isFinite(target.timeoutMs)
|
|
166
|
+
|| target.timeoutMs <= 0)) {
|
|
167
|
+
return 'target.timeoutMs must be a positive finite number';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (['value-change', 'value-equals', 'value-matches'].includes(context.condition)) {
|
|
172
|
+
if (!targetObject || typeof target.extract !== 'string' || !target.extract.trim()) {
|
|
173
|
+
return `target.extract is required for ${context.condition}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (context.condition === 'value-equals'
|
|
177
|
+
&& (!Object.prototype.hasOwnProperty.call(target, 'expected')
|
|
178
|
+
|| target.expected === undefined)) {
|
|
179
|
+
return 'target.expected is required for value-equals';
|
|
180
|
+
}
|
|
181
|
+
if (context.condition === 'value-matches'
|
|
182
|
+
&& (typeof target.regex !== 'string' || !target.regex)) {
|
|
183
|
+
return 'target.regex is required for value-matches';
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Substitute {{env.VAR}} in a single header value. Returns the substituted
|
|
189
|
+
// string when every referenced env var resolved, or { _missingEnv } when any
|
|
190
|
+
// referenced var is unset — caller drops the header and emits a WARN.
|
|
191
|
+
function _substituteEnvHeader(value) {
|
|
192
|
+
const re = /\{\{\s*env\.([A-Z_][A-Z0-9_]*)\s*\}\}/g;
|
|
193
|
+
let missing = null;
|
|
194
|
+
const out = String(value).replace(re, (m, k) => {
|
|
195
|
+
const v = process.env[k];
|
|
196
|
+
if (v === undefined || v === '') {
|
|
197
|
+
if (missing === null) missing = k;
|
|
198
|
+
return m;
|
|
199
|
+
}
|
|
200
|
+
return v;
|
|
201
|
+
});
|
|
202
|
+
return missing === null ? out : { _missingEnv: missing };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// JSONPath-lite accessor: `$.foo[0].bar` or `$.foo['weird key']`. Tokens are
|
|
206
|
+
// `.field`, `[N]`, or `['key']`. Returns null on parse-failure or any miss.
|
|
207
|
+
function _extractJsonPath(jpath, body) {
|
|
208
|
+
if (typeof body === 'string') {
|
|
209
|
+
try { body = JSON.parse(body); } catch { return null; }
|
|
210
|
+
}
|
|
211
|
+
if (!jpath || jpath[0] !== '$') return null;
|
|
212
|
+
const tokens = jpath.slice(1).match(/\.[a-zA-Z_$][\w$]*|\[\d+\]|\['[^']*'\]|\["[^"]*"\]/g) || [];
|
|
213
|
+
let cursor = body;
|
|
214
|
+
for (const tok of tokens) {
|
|
215
|
+
if (cursor === null || cursor === undefined) return null;
|
|
216
|
+
if (tok[0] === '.') cursor = cursor[tok.slice(1)];
|
|
217
|
+
else if (tok[1] === "'" || tok[1] === '"') cursor = cursor[tok.slice(2, -2)];
|
|
218
|
+
else cursor = cursor[parseInt(tok.slice(1, -1), 10)];
|
|
219
|
+
}
|
|
220
|
+
return cursor === undefined ? null : cursor;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function _parseRegexLiteral(s) {
|
|
224
|
+
const m = s.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
225
|
+
if (!m) return null;
|
|
226
|
+
return { pattern: m[1], flags: m[2] || '' };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _extractRegex(rePattern, flags, rawBody) {
|
|
230
|
+
let re;
|
|
231
|
+
try { re = new RegExp(rePattern, flags || ''); } catch { return null; }
|
|
232
|
+
const m = String(rawBody).match(re);
|
|
233
|
+
if (!m) return null;
|
|
234
|
+
return m.length > 1 ? m[1] : m[0];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function _extract(extract, rawBody) {
|
|
238
|
+
if (!extract) return null;
|
|
239
|
+
if (typeof extract === 'string') {
|
|
240
|
+
if (extract.startsWith('$')) return _extractJsonPath(extract, rawBody);
|
|
241
|
+
const lit = _parseRegexLiteral(extract);
|
|
242
|
+
if (lit) return _extractRegex(lit.pattern, lit.flags, rawBody);
|
|
243
|
+
// Bare property name → treat as $.name for ergonomics.
|
|
244
|
+
return _extractJsonPath('$.' + extract, rawBody);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Default Node http(s) fetcher. Returns { status, headers, body }.
|
|
250
|
+
function _defaultHttpFetch(cfg) {
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
let parsed;
|
|
253
|
+
try { parsed = new URL(cfg.url); } catch (err) { reject(err); return; }
|
|
254
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
255
|
+
const opts = {
|
|
256
|
+
method: cfg.method,
|
|
257
|
+
hostname: parsed.hostname,
|
|
258
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
259
|
+
path: (parsed.pathname || '/') + (parsed.search || ''),
|
|
260
|
+
headers: { 'User-Agent': 'minions-watch', ...cfg.headers },
|
|
261
|
+
timeout: cfg.timeoutMs,
|
|
262
|
+
};
|
|
263
|
+
const req = lib.request(opts, (res) => {
|
|
264
|
+
const chunks = [];
|
|
265
|
+
let bytes = 0;
|
|
266
|
+
// Cap captured bytes at 4× truncation budget so we can still parse the
|
|
267
|
+
// first 4KB of valid JSON without buffering megabyte payloads.
|
|
268
|
+
const cap = MAX_BODY_BYTES * 4;
|
|
269
|
+
res.on('data', (c) => {
|
|
270
|
+
if (bytes >= cap) return;
|
|
271
|
+
chunks.push(c);
|
|
272
|
+
bytes += c.length;
|
|
273
|
+
});
|
|
274
|
+
res.on('end', () => {
|
|
275
|
+
resolve({
|
|
276
|
+
status: res.statusCode,
|
|
277
|
+
headers: res.headers || {},
|
|
278
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
req.on('error', reject);
|
|
283
|
+
req.on('timeout', () => { req.destroy(new Error(`fetch timeout after ${cfg.timeoutMs}ms`)); });
|
|
284
|
+
if (cfg.body !== null && cfg.body !== undefined) {
|
|
285
|
+
req.write(typeof cfg.body === 'string' ? cfg.body : JSON.stringify(cfg.body));
|
|
286
|
+
}
|
|
287
|
+
req.end();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function _doFetch(target) {
|
|
292
|
+
const cfg = _resolveCfg(target);
|
|
293
|
+
if (!cfg) {
|
|
294
|
+
return {
|
|
295
|
+
url: '', status: 0, body: '', rawBody: '', extracted: null,
|
|
296
|
+
fetchedAt: ts(), durationMs: 0, error: 'invalid target',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const headers = {};
|
|
300
|
+
for (const [k, v] of Object.entries(cfg.headers)) {
|
|
301
|
+
const subbed = _substituteEnvHeader(v);
|
|
302
|
+
if (subbed && typeof subbed === 'object' && subbed._missingEnv) {
|
|
303
|
+
_warn(`watch-plugins/http: header '${k}' dropped — env var ${subbed._missingEnv} not set in process.env`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
headers[k] = subbed;
|
|
307
|
+
}
|
|
308
|
+
const start = Date.now();
|
|
309
|
+
let result, err;
|
|
310
|
+
try {
|
|
311
|
+
result = await _fetcher({ ...cfg, headers });
|
|
312
|
+
} catch (e) {
|
|
313
|
+
err = e;
|
|
314
|
+
}
|
|
315
|
+
const durationMs = Date.now() - start;
|
|
316
|
+
if (err) {
|
|
317
|
+
return {
|
|
318
|
+
url: cfg.url, status: 0, body: '', rawBody: '', extracted: null,
|
|
319
|
+
fetchedAt: ts(), durationMs, error: err.message || String(err),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const fullBody = String(result.body || '');
|
|
323
|
+
const rawBody = fullBody.length > MAX_BODY_BYTES ? fullBody.slice(0, MAX_BODY_BYTES) : fullBody;
|
|
324
|
+
const extracted = cfg.extract ? _extract(cfg.extract, fullBody) : null;
|
|
325
|
+
return {
|
|
326
|
+
url: cfg.url,
|
|
327
|
+
status: typeof result.status === 'number' ? result.status : 0,
|
|
328
|
+
body: rawBody,
|
|
329
|
+
rawBody,
|
|
330
|
+
extracted,
|
|
331
|
+
fetchedAt: ts(),
|
|
332
|
+
durationMs,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _scheduleRefresh(target, key) {
|
|
337
|
+
if (_inFlight.has(key)) return;
|
|
338
|
+
_inFlight.add(key);
|
|
339
|
+
Promise.resolve()
|
|
340
|
+
.then(() => _doFetch(target))
|
|
341
|
+
.then((entity) => { _cache.set(key, entity); })
|
|
342
|
+
.catch((e) => {
|
|
343
|
+
_cache.set(key, {
|
|
344
|
+
url: '', status: 0, body: '', rawBody: '', extracted: null,
|
|
345
|
+
fetchedAt: ts(), durationMs: 0, error: e && e.message ? e.message : String(e),
|
|
346
|
+
});
|
|
347
|
+
})
|
|
348
|
+
.then(() => { _inFlight.delete(key); });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function fetchEntity(target, state) {
|
|
352
|
+
const key = _targetKey(target);
|
|
353
|
+
// Test/external override path — state.httpResponses[key] short-circuits the
|
|
354
|
+
// background-fetch cache so unit tests can drive the plugin deterministically.
|
|
355
|
+
if (state && state.httpResponses && Object.prototype.hasOwnProperty.call(state.httpResponses, key)) {
|
|
356
|
+
return { ...state.httpResponses[key], _target: target };
|
|
357
|
+
}
|
|
358
|
+
const cached = _cache.get(key);
|
|
359
|
+
const cachedAge = cached && cached.fetchedAt ? Date.now() - new Date(cached.fetchedAt).getTime() : Infinity;
|
|
360
|
+
if (!cached || cachedAge > CACHE_FRESHNESS_MS) {
|
|
361
|
+
_scheduleRefresh(target, key);
|
|
362
|
+
}
|
|
363
|
+
return cached ? { ...cached, _target: target } : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function captureState(entity) {
|
|
367
|
+
if (!entity) return {};
|
|
368
|
+
return {
|
|
369
|
+
httpStatus: entity.status,
|
|
370
|
+
url: entity.url,
|
|
371
|
+
extracted: entity.extracted,
|
|
372
|
+
fetchedAt: entity.fetchedAt,
|
|
373
|
+
durationMs: entity.durationMs,
|
|
374
|
+
rawBody: entity.rawBody,
|
|
375
|
+
error: entity.error || null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function _deepEqual(a, b) {
|
|
380
|
+
if (a === b) return true;
|
|
381
|
+
if (a === null || b === null || a === undefined || b === undefined) return false;
|
|
382
|
+
if (typeof a !== typeof b) return false;
|
|
383
|
+
if (typeof a !== 'object') return a === b;
|
|
384
|
+
try { return JSON.stringify(a) === JSON.stringify(b); }
|
|
385
|
+
catch { return false; }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function evaluate(condition, entity, prevState, target) {
|
|
389
|
+
const cur = captureState(entity);
|
|
390
|
+
switch (condition) {
|
|
391
|
+
case 'status-change': {
|
|
392
|
+
if (prevState.httpStatus === undefined) return { triggered: false, message: '' };
|
|
393
|
+
const changed = prevState.httpStatus !== cur.httpStatus;
|
|
394
|
+
return { triggered: changed, message: changed ? `HTTP ${cur.url}: status ${prevState.httpStatus} → ${cur.httpStatus}` : '' };
|
|
395
|
+
}
|
|
396
|
+
case 'value-change': {
|
|
397
|
+
if (!('extracted' in prevState)) return { triggered: false, message: '' };
|
|
398
|
+
const changed = !_deepEqual(prevState.extracted, cur.extracted);
|
|
399
|
+
const fmt = (v) => { try { return JSON.stringify(v); } catch { return String(v); } };
|
|
400
|
+
return { triggered: changed, message: changed ? `HTTP ${cur.url}: extracted changed: ${fmt(prevState.extracted)} → ${fmt(cur.extracted)}` : '' };
|
|
401
|
+
}
|
|
402
|
+
case 'value-equals': {
|
|
403
|
+
const expected = (target && typeof target === 'object' && Object.prototype.hasOwnProperty.call(target, 'expected'))
|
|
404
|
+
? target.expected : undefined;
|
|
405
|
+
if (expected === undefined) return { triggered: false, message: '' };
|
|
406
|
+
const matched = cur.extracted === expected;
|
|
407
|
+
return { triggered: matched, message: matched ? `HTTP ${cur.url}: extracted equals expected (${expected})` : '' };
|
|
408
|
+
}
|
|
409
|
+
case 'value-matches': {
|
|
410
|
+
const pattern = (target && typeof target === 'object' && typeof target.regex === 'string') ? target.regex : null;
|
|
411
|
+
if (!pattern || cur.extracted === null || cur.extracted === undefined) return { triggered: false, message: '' };
|
|
412
|
+
let re;
|
|
413
|
+
try { re = new RegExp(pattern); } catch { return { triggered: false, message: '' }; }
|
|
414
|
+
const matched = re.test(String(cur.extracted));
|
|
415
|
+
return { triggered: matched, message: matched ? `HTTP ${cur.url}: extracted matches /${pattern}/` : '' };
|
|
416
|
+
}
|
|
417
|
+
case 'http-error': {
|
|
418
|
+
const errored = !!cur.error || (typeof cur.httpStatus === 'number' && cur.httpStatus >= 400);
|
|
419
|
+
const detail = cur.error ? cur.error : `status ${cur.httpStatus}`;
|
|
420
|
+
return { triggered: errored, message: errored ? `HTTP ${cur.url}: ${detail}` : '' };
|
|
421
|
+
}
|
|
422
|
+
default:
|
|
423
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// contextVars supplements the action template ctx with plugin-specific aliases.
|
|
428
|
+
// httpStatus/url/extracted/fetchedAt/durationMs/rawBody auto-promote from
|
|
429
|
+
// newState scalars in buildTriggerContext, but prevExtracted is on prevState
|
|
430
|
+
// and an object-shaped extracted value would otherwise be unreachable as a
|
|
431
|
+
// scalar — covering both here keeps the templating contract uniform.
|
|
432
|
+
function contextVars(entity, prevState, newState) {
|
|
433
|
+
const out = {};
|
|
434
|
+
if (newState) {
|
|
435
|
+
if (newState.httpStatus !== undefined) out.httpStatus = newState.httpStatus;
|
|
436
|
+
if (newState.url !== undefined) out.url = newState.url;
|
|
437
|
+
if (newState.extracted !== undefined && newState.extracted !== null) out.extracted = newState.extracted;
|
|
438
|
+
if (newState.fetchedAt !== undefined) out.fetchedAt = newState.fetchedAt;
|
|
439
|
+
if (newState.durationMs !== undefined) out.durationMs = newState.durationMs;
|
|
440
|
+
if (newState.rawBody !== undefined) out.rawBody = newState.rawBody;
|
|
441
|
+
}
|
|
442
|
+
if (prevState && prevState.extracted !== undefined && prevState.extracted !== null) {
|
|
443
|
+
out.prevExtracted = prevState.extracted;
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = {
|
|
449
|
+
name: 'http',
|
|
450
|
+
spec: {
|
|
451
|
+
label: 'HTTP Endpoint',
|
|
452
|
+
description: 'Poll any URL, extract a value, compare against previous state.',
|
|
453
|
+
conditions: ['status-change', 'value-change', 'value-equals', 'value-matches', 'http-error'],
|
|
454
|
+
absoluteConditions: [], // all change-based — stopAfter:0 truly runs forever
|
|
455
|
+
validateTarget: _validateTarget,
|
|
456
|
+
fetchEntity,
|
|
457
|
+
captureState,
|
|
458
|
+
evaluate,
|
|
459
|
+
contextVars,
|
|
460
|
+
},
|
|
461
|
+
// Test hooks (unit tests only — not part of the plugin contract).
|
|
462
|
+
setFetcher,
|
|
463
|
+
_resetCache,
|
|
464
|
+
_resolveCfg,
|
|
465
|
+
_targetKey,
|
|
466
|
+
_extract,
|
|
467
|
+
_extractJsonPath,
|
|
468
|
+
_extractRegex,
|
|
469
|
+
_parseRegexLiteral,
|
|
470
|
+
_substituteEnvHeader,
|
|
471
|
+
_doFetch,
|
|
472
|
+
DEFAULT_TIMEOUT_MS,
|
|
473
|
+
MAX_BODY_BYTES,
|
|
474
|
+
};
|