mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-26 01:01:58 +08:00
Compare commits
1 Commits
feature/sk
...
fix/llama-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
806e796cc2 |
@@ -146,7 +146,7 @@ Default guidance:
|
||||
|
||||
Default Completeness bands:
|
||||
|
||||
- `Clawesome` (95-100): complete across expected workflows, variants, and
|
||||
- `Lovable` (95-100): complete across expected workflows, variants, and
|
||||
recovery branches, with only minor polish gaps.
|
||||
- `Stable` (80-95): the expected workflow set is broadly present, with only
|
||||
bounded missing branches.
|
||||
@@ -172,7 +172,7 @@ Default Completeness bands:
|
||||
|
||||
Bands:
|
||||
|
||||
- `Clawesome`: 95-100
|
||||
- `Lovable`: 95-100
|
||||
- `Stable`: 80-95
|
||||
- `Beta`: 70-80
|
||||
- `Alpha`: 50-70
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# OpenClaw Maturity Scorecard Agent
|
||||
|
||||
You are refreshing the OpenClaw maturity score source for a release scorecard.
|
||||
|
||||
Goal: use the `$claw-score` skill to refresh `qa/maturity-scores.yaml` for every active surface in `taxonomy.yaml`, using the current repository and the release evidence artifacts in `.artifacts/maturity-evidence`.
|
||||
|
||||
Allowed tracked paths:
|
||||
|
||||
- `qa/maturity-scores.yaml`
|
||||
|
||||
Hard limits:
|
||||
|
||||
- Do not edit generated docs, taxonomy, workflows, scripts, package metadata, lockfiles, tests, or application code.
|
||||
- Do not render docs. The workflow renders docs after validating the score source.
|
||||
- Keep the score source schema valid for QA Lab maturity score validation.
|
||||
|
||||
Required workflow:
|
||||
|
||||
1. Use the `$claw-score` skill before editing.
|
||||
2. Read `taxonomy.yaml`, any existing maturity score file, and the release evidence artifacts.
|
||||
3. Refresh scores for every active surface in `taxonomy.yaml`.
|
||||
4. Run the QA Lab maturity score validation used by this repository.
|
||||
5. If no defensible score update is possible, leave a valid `qa/maturity-scores.yaml` and explain the uncertainty in the final message.
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -1177,9 +1177,7 @@ jobs:
|
||||
timeout-minutes: ${{ matrix.timeout_minutes || 60 }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# The canonical main path waits for the admission debounce above, so
|
||||
# modestly widen this large matrix without recreating registration bursts.
|
||||
max-parallel: 16
|
||||
max-parallel: 12
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
18
.github/workflows/codeql.yml
vendored
18
.github/workflows/codeql.yml
vendored
@@ -22,6 +22,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/actions/**"
|
||||
- ".github/codeql/**"
|
||||
- ".github/workflows/**"
|
||||
- "packages/**"
|
||||
- "src/**"
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
|
||||
@@ -49,32 +55,32 @@ jobs:
|
||||
include:
|
||||
- language: javascript-typescript
|
||||
category: core-auth-secrets
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-core-auth-secrets-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: channel-runtime-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-channel-runtime-boundary-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: network-ssrf-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-network-ssrf-boundary-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: mcp-process-tool-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: plugin-trust-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-plugin-trust-boundary-critical-security.yml
|
||||
- language: actions
|
||||
category: actions
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout_minutes: 10
|
||||
config_file: ./.github/codeql/codeql-actions-critical-security.yml
|
||||
steps:
|
||||
|
||||
4
.github/workflows/crabbox-hydrate.yml
vendored
4
.github/workflows/crabbox-hydrate.yml
vendored
@@ -490,7 +490,7 @@ jobs:
|
||||
if (-not $env:CRABBOX_ID -or $env:CRABBOX_ID -notmatch '^[A-Za-z0-9._-]+$') {
|
||||
Write-Error "Invalid crabbox_id"
|
||||
}
|
||||
$actionsRoot = "C:\ProgramData\crabbox\actions"
|
||||
$actionsRoot = Join-Path $HOME ".crabbox\actions"
|
||||
New-Item -ItemType Directory -Force $actionsRoot | Out-Null
|
||||
$state = Join-Path $actionsRoot "$env:CRABBOX_ID.env"
|
||||
$envFile = Join-Path $actionsRoot "$env:CRABBOX_ID.env.ps1"
|
||||
@@ -546,7 +546,7 @@ jobs:
|
||||
if ($env:CRABBOX_KEEP_ALIVE_MINUTES -match '^[0-9]+$') {
|
||||
$minutes = [int]$env:CRABBOX_KEEP_ALIVE_MINUTES
|
||||
}
|
||||
$stop = Join-Path "C:\ProgramData\crabbox\actions" "$env:CRABBOX_ID.stop"
|
||||
$stop = Join-Path $HOME ".crabbox\actions\$env:CRABBOX_ID.stop"
|
||||
$deadline = (Get-Date).AddMinutes($minutes)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path $stop) {
|
||||
|
||||
116
.github/workflows/maturity-scorecard.yml
vendored
116
.github/workflows/maturity-scorecard.yml
vendored
@@ -12,40 +12,6 @@ on:
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
expected_sha:
|
||||
description: Optional full SHA that ref must resolve to
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
qa_evidence_run_id:
|
||||
description: Optional workflow run id containing qa-evidence.json
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
ref:
|
||||
description: OpenClaw branch, tag, or SHA containing the maturity score source
|
||||
required: true
|
||||
type: string
|
||||
expected_sha:
|
||||
description: Optional full SHA that ref must resolve to
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
secrets:
|
||||
OPENAI_API_KEY:
|
||||
description: OpenAI API key used by live QA profile scenarios
|
||||
required: true
|
||||
OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY:
|
||||
description: Optional OpenAI API key used by maturity scorecard agent steps
|
||||
required: false
|
||||
GH_APP_PRIVATE_KEY:
|
||||
description: Optional GitHub App private key for generated docs PR creation
|
||||
required: false
|
||||
GH_APP_PRIVATE_KEY_FALLBACK:
|
||||
description: Optional fallback GitHub App private key for generated docs PR creation
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -77,25 +43,14 @@ jobs:
|
||||
- name: Validate selected ref
|
||||
id: validate
|
||||
env:
|
||||
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||
INPUT_REF: ${{ inputs.ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
selected_revision="$(git rev-parse HEAD)"
|
||||
expected_sha="${EXPECTED_SHA,,}"
|
||||
trusted_reason=""
|
||||
|
||||
if [[ -n "${expected_sha// }" && ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "expected_sha must be a full 40-character SHA; got: ${EXPECTED_SHA}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "${expected_sha// }" && "${selected_revision,,}" != "$expected_sha" ]]; then
|
||||
echo "Ref '${INPUT_REF}' resolved to ${selected_revision}, expected ${EXPECTED_SHA}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
|
||||
@@ -133,7 +88,6 @@ jobs:
|
||||
uses: ./.github/workflows/qa-profile-evidence.yml
|
||||
with:
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
expected_sha: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
qa_profile: all
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -264,67 +218,6 @@ jobs:
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Ensure maturity scorecard agent key exists
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
||||
echo "Missing OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run Codex maturity scorecard agent
|
||||
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
|
||||
env:
|
||||
MATURITY_EVIDENCE_DIR: .artifacts/maturity-evidence
|
||||
MATURITY_SCORES_PATH: qa/maturity-scores.yaml
|
||||
MATURITY_TAXONOMY_PATH: taxonomy.yaml
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
prompt-file: .github/codex/prompts/maturity-scorecard-agent.md
|
||||
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
effort: high
|
||||
sandbox: workspace-write
|
||||
safety-strategy: drop-sudo
|
||||
|
||||
- name: Enforce focused maturity score patch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git restore --staged :/
|
||||
|
||||
allowed='^qa/maturity-scores\.yaml$'
|
||||
bad_tracked="$(
|
||||
git diff --name-only HEAD -- | while IFS= read -r path; do
|
||||
if [[ ! "$path" =~ $allowed ]]; then
|
||||
printf '%s\n' "$path"
|
||||
fi
|
||||
done
|
||||
)"
|
||||
if [[ -n "$bad_tracked" ]]; then
|
||||
echo "Maturity scorecard agent touched forbidden tracked paths:"
|
||||
printf '%s\n' "$bad_tracked"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bad_untracked="$(
|
||||
git ls-files --others --exclude-standard | while IFS= read -r path; do
|
||||
if [[ "$path" != "qa/maturity-scores.yaml" ]]; then
|
||||
printf '%s\n' "$path"
|
||||
fi
|
||||
done
|
||||
)"
|
||||
if [[ -n "$bad_untracked" ]]; then
|
||||
echo "Maturity scorecard agent created forbidden untracked paths:"
|
||||
printf '%s\n' "$bad_untracked"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f qa/maturity-scores.yaml ]]; then
|
||||
echo "Maturity scorecard agent must produce qa/maturity-scores.yaml." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate maturity score sources
|
||||
run: |
|
||||
node --import tsx --input-type=module <<'NODE'
|
||||
@@ -367,7 +260,6 @@ jobs:
|
||||
--strict-inputs
|
||||
|
||||
- name: Create generated docs PR app token
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
id: app-token
|
||||
continue-on-error: true
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
||||
@@ -378,7 +270,7 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Create generated docs PR fallback app token
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && steps.app-token.outcome == 'failure' }}
|
||||
if: ${{ steps.app-token.outcome == 'failure' }}
|
||||
id: app-token-fallback
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
||||
with:
|
||||
@@ -388,7 +280,6 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Open generated docs PR
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
|
||||
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
|
||||
@@ -400,7 +291,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$(git status --porcelain -- qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md)" ]]; then
|
||||
if [[ -z "$(git status --porcelain -- qa/maturity-scores.yaml docs/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md)" ]]; then
|
||||
{
|
||||
echo
|
||||
echo "- Pull request: skipped; generated scorecard matches selected ref"
|
||||
@@ -420,6 +311,9 @@ jobs:
|
||||
git fetch --no-tags --depth=1 origin "refs/heads/${branch}:refs/remotes/origin/${branch}" || true
|
||||
git switch -C "$branch"
|
||||
git add qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md
|
||||
if git ls-files --error-unmatch docs/maturity-scores.yaml >/dev/null 2>&1 || [[ -e docs/maturity-scores.yaml ]]; then
|
||||
git add docs/maturity-scores.yaml
|
||||
fi
|
||||
git commit -m "docs: update maturity scorecard"
|
||||
git push --force-with-lease origin "$branch"
|
||||
|
||||
|
||||
36
.github/workflows/openclaw-release-checks.yml
vendored
36
.github/workflows/openclaw-release-checks.yml
vendored
@@ -767,20 +767,6 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
|
||||
maturity_scorecard_release_checks:
|
||||
name: Render maturity scorecard release docs
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa"]'), needs.resolve_target.outputs.rerun_group)
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
uses: ./.github/workflows/maturity-scorecard.yml
|
||||
with:
|
||||
ref: ${{ needs.resolve_target.outputs.ref }}
|
||||
expected_sha: ${{ needs.resolve_target.outputs.revision }}
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
qa_lab_parity_lane_release_checks:
|
||||
name: Run QA Lab parity lane (${{ matrix.lane }})
|
||||
needs: [resolve_target]
|
||||
@@ -867,7 +853,7 @@ jobs:
|
||||
name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -973,7 +959,7 @@ jobs:
|
||||
name: release-qa-parity-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1145,7 +1131,7 @@ jobs:
|
||||
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1255,13 +1241,13 @@ jobs:
|
||||
--output .artifacts/qa-e2e/runtime-parity-standard-report/qa-runtime-tool-coverage-report.md
|
||||
|
||||
- name: Upload runtime tool coverage artifacts
|
||||
if: ${{ always() && steps.verify_runtime_parity_status.outputs.ready == 'true' }}
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: release-qa-runtime-tool-coverage-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/runtime-parity-standard-report/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
qa_live_matrix_release_checks:
|
||||
name: Run QA Lab live Matrix lane
|
||||
@@ -1341,7 +1327,7 @@ jobs:
|
||||
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1481,7 +1467,7 @@ jobs:
|
||||
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1621,7 +1607,7 @@ jobs:
|
||||
name: release-qa-live-discord-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1764,7 +1750,7 @@ jobs:
|
||||
name: release-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1904,7 +1890,7 @@ jobs:
|
||||
name: release-qa-live-slack-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1960,7 +1946,6 @@ jobs:
|
||||
- docker_e2e_release_checks
|
||||
- package_acceptance_release_checks
|
||||
- qa_lab_parity_lane_release_checks
|
||||
- maturity_scorecard_release_checks
|
||||
- qa_lab_parity_report_release_checks
|
||||
- qa_lab_runtime_parity_release_checks
|
||||
- runtime_tool_coverage_release_checks
|
||||
@@ -2046,7 +2031,6 @@ jobs:
|
||||
"docker_e2e_release_checks=${{ needs.docker_e2e_release_checks.result }}" \
|
||||
"package_acceptance_release_checks=${{ needs.package_acceptance_release_checks.result }}" \
|
||||
"qa_lab_parity_lane_release_checks=${{ needs.qa_lab_parity_lane_release_checks.result }}" \
|
||||
"maturity_scorecard_release_checks=${{ needs.maturity_scorecard_release_checks.result }}" \
|
||||
"qa_lab_parity_report_release_checks=${{ needs.qa_lab_parity_report_release_checks.result }}" \
|
||||
"qa_lab_runtime_parity_release_checks=${{ needs.qa_lab_runtime_parity_release_checks.result }}" \
|
||||
"runtime_tool_coverage_release_checks=${{ needs.runtime_tool_coverage_release_checks.result }}" \
|
||||
|
||||
@@ -1466,9 +1466,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload postpublish evidence
|
||||
if: ${{ always() && inputs.publish_openclaw_npm }}
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: openclaw-release-postpublish-evidence-${{ inputs.tag }}
|
||||
path: ${{ runner.temp }}/openclaw-release-postpublish-evidence
|
||||
if-no-files-found: error
|
||||
if-no-files-found: ignore
|
||||
|
||||
2
.github/workflows/opengrep-precise-full.yml
vendored
2
.github/workflows/opengrep-precise-full.yml
vendored
@@ -66,5 +66,5 @@ jobs:
|
||||
with:
|
||||
name: opengrep-full-sarif
|
||||
path: .opengrep-out/precise.sarif
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
2
.github/workflows/opengrep-precise.yml
vendored
2
.github/workflows/opengrep-precise.yml
vendored
@@ -97,5 +97,5 @@ jobs:
|
||||
with:
|
||||
name: opengrep-pr-diff-sarif
|
||||
path: .opengrep-out/precise.sarif
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
3
.github/workflows/qa-profile-evidence.yml
vendored
3
.github/workflows/qa-profile-evidence.yml
vendored
@@ -243,9 +243,6 @@ jobs:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Ensure Playwright Chromium
|
||||
run: node scripts/ensure-playwright-chromium.mjs
|
||||
|
||||
- name: Run QA profile
|
||||
id: run_profile
|
||||
env:
|
||||
|
||||
@@ -37,7 +37,6 @@ This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs.
|
||||
|
||||
#### Pull requests
|
||||
|
||||
- **PR #92154** fix(qqbot): gate private group commands and close strict command visibility gaps. Thanks @sliverp.
|
||||
- **PR #90463** refactor: add session accessor seam with gateway consumer. Thanks @jalehman.
|
||||
- **PR #88656** Drop reasoning-only length turns from replay. Thanks @abel-zer0.
|
||||
- **PR #92856** feat(webui): add session workspace rail. Thanks @Solvely-Colin.
|
||||
|
||||
@@ -152,7 +152,6 @@ extension SettingsProTab {
|
||||
}
|
||||
let notificationSettings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
self.applyNotificationStatus(notificationSettings.authorizationStatus)
|
||||
self.registerForRemoteNotificationsIfEnrollmentReady()
|
||||
|
||||
let issueCount = SettingsDiagnostics.issueCount(
|
||||
gatewayConnected: self.gatewayDiagnosticConnected,
|
||||
@@ -418,7 +417,6 @@ extension SettingsProTab {
|
||||
let status = settings.authorizationStatus
|
||||
Task { @MainActor in
|
||||
self.applyNotificationStatus(status)
|
||||
self.registerForRemoteNotificationsIfEnrollmentReady()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,7 +437,6 @@ extension SettingsProTab {
|
||||
|
||||
func requestNotificationAuthorizationFromSettings() {
|
||||
guard !self.isRequestingNotificationAuthorization else { return }
|
||||
PushEnrollmentConsent.markDisclosureAccepted()
|
||||
self.isRequestingNotificationAuthorization = true
|
||||
Task {
|
||||
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
|
||||
@@ -451,19 +448,12 @@ extension SettingsProTab {
|
||||
await MainActor.run {
|
||||
self.isRequestingNotificationAuthorization = false
|
||||
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
|
||||
guard granted else { return }
|
||||
self.registerForRemoteNotificationsIfEnrollmentReady()
|
||||
guard granted, self.notificationStatus.allowsNotifications else { return }
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func registerForRemoteNotificationsIfEnrollmentReady() {
|
||||
guard PushEnrollmentConsent.disclosureAccepted else { return }
|
||||
guard self.notificationStatus.allowsNotifications else { return }
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func applyNotificationStatus(_ status: UNAuthorizationStatus) {
|
||||
self.notificationStatus = SettingsNotificationStatus(status)
|
||||
|
||||
@@ -4103,9 +4103,6 @@ extension NodeAppModel {
|
||||
|
||||
private func registerAPNsTokenIfNeeded() async {
|
||||
let usesRelayTransport = await self.pushRegistrationManager.usesRelayTransport
|
||||
guard await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport) else {
|
||||
return
|
||||
}
|
||||
guard self.gatewayConnected else {
|
||||
if usesRelayTransport {
|
||||
GatewayDiagnostics.pushRelay.skipped("gateway_offline")
|
||||
@@ -4166,23 +4163,6 @@ extension NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func canPublishAPNsRegistration(usesRelayTransport: Bool) async -> Bool {
|
||||
guard PushEnrollmentConsent.disclosureAccepted else {
|
||||
if usesRelayTransport {
|
||||
GatewayDiagnostics.pushRelay.skipped("enrollment_disclosure_not_accepted")
|
||||
}
|
||||
return false
|
||||
}
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard Self.isNotificationAuthorizationAllowed(status) else {
|
||||
if usesRelayTransport {
|
||||
GatewayDiagnostics.pushRelay.skipped("notifications_not_authorized")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func fetchPushRelayGatewayIdentity() async throws -> PushRelayGatewayIdentity {
|
||||
let response = try await self.operatorGateway.request(
|
||||
method: "gateway.identity.get",
|
||||
@@ -5146,10 +5126,6 @@ extension NodeAppModel {
|
||||
self.setOperatorConnected(connected)
|
||||
}
|
||||
|
||||
func _test_canPublishAPNsRegistration(usesRelayTransport: Bool = true) async -> Bool {
|
||||
await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport)
|
||||
}
|
||||
|
||||
nonisolated static func _test_makeWatchChatItems(from raw: [OpenClawKit.AnyCodable]) -> [OpenClawWatchChatItem] {
|
||||
self.makeWatchChatItems(from: raw)
|
||||
}
|
||||
|
||||
@@ -123,28 +123,8 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.delegate = self
|
||||
ExecApprovalNotificationBridge.registerCategory(center: notificationCenter)
|
||||
Task { @MainActor in
|
||||
await self.registerForRemoteNotificationsIfEnrollmentReady(application)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func registerForRemoteNotificationsIfEnrollmentReady(_ application: UIApplication) async {
|
||||
guard PushEnrollmentConsent.disclosureAccepted else { return }
|
||||
guard await Self.isNotificationAuthorizationAllowed() else { return }
|
||||
application.registerForRemoteNotifications()
|
||||
}
|
||||
|
||||
private static func isNotificationAuthorizationAllowed() async -> Bool {
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
switch settings.authorizationStatus {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
return true
|
||||
case .denied, .notDetermined:
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
enum PushEnrollmentConsent {
|
||||
static let disclosureAcceptedKey = "push.enrollment.disclosureAccepted"
|
||||
|
||||
static var disclosureAccepted: Bool {
|
||||
UserDefaults.standard.bool(forKey: disclosureAcceptedKey)
|
||||
}
|
||||
|
||||
static func markDisclosureAccepted() {
|
||||
UserDefaults.standard.set(true, forKey: self.disclosureAcceptedKey)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static func reset() {
|
||||
UserDefaults.standard.removeObject(forKey: self.disclosureAcceptedKey)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -76,7 +76,6 @@ Sources/Permissions/PermissionRequestBridge.swift
|
||||
Sources/Push/ExecApprovalNotificationBridge.swift
|
||||
Sources/Push/BackgroundAliveBeacon.swift
|
||||
Sources/Push/PushBuildConfig.swift
|
||||
Sources/Push/PushEnrollmentConsent.swift
|
||||
Sources/Push/PushRegistrationManager.swift
|
||||
Sources/Push/PushRelayClient.swift
|
||||
Sources/Push/PushRelayKeychainStore.swift
|
||||
|
||||
@@ -1377,24 +1377,6 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
#expect(center.addCalls == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func apnsRegistrationRequiresDisclosureAndNotificationAuthorization() async {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .authorized
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
PushEnrollmentConsent.reset()
|
||||
defer { PushEnrollmentConsent.reset() }
|
||||
|
||||
#expect(await appModel._test_canPublishAPNsRegistration() == false)
|
||||
#expect(await appModel._test_canPublishAPNsRegistration(usesRelayTransport: false) == false)
|
||||
|
||||
PushEnrollmentConsent.markDisclosureAccepted()
|
||||
center.status = .notDetermined
|
||||
#expect(await appModel._test_canPublishAPNsRegistration() == false)
|
||||
|
||||
center.status = .authorized
|
||||
#expect(await appModel._test_canPublishAPNsRegistration())
|
||||
}
|
||||
|
||||
@Test @MainActor func chatPushWithoutSpeechReturnsUnavailableWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
|
||||
@@ -550,20 +550,6 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
|
||||
}
|
||||
|
||||
@Test func `push enrollment stays behind notification disclosure flow`() throws {
|
||||
let appSource = try String(contentsOf: Self.openClawAppSourceURL(), encoding: .utf8)
|
||||
let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)
|
||||
let modelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(appSource.contains("PushEnrollmentConsent.disclosureAccepted"))
|
||||
#expect(appSource.contains("await Self.isNotificationAuthorizationAllowed()"))
|
||||
#expect(actionsSource.contains("PushEnrollmentConsent.markDisclosureAccepted()"))
|
||||
#expect(actionsSource.contains("self.registerForRemoteNotificationsIfEnrollmentReady()"))
|
||||
#expect(modelSource.contains("PushEnrollmentConsent.disclosureAccepted"))
|
||||
#expect(modelSource.contains("notifications_not_authorized"))
|
||||
#expect(modelSource.contains("enrollment_disclosure_not_accepted"))
|
||||
}
|
||||
|
||||
@Test func `gateway settings keeps pairing trust diagnostics and tailscale actions`() throws {
|
||||
let settingsSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
|
||||
let sectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
@@ -800,13 +786,6 @@ struct RootTabsSourceGuardTests {
|
||||
.appendingPathComponent("Sources/Design/SettingsProTab.swift")
|
||||
}
|
||||
|
||||
private static func openClawAppSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Sources/OpenClawApp.swift")
|
||||
}
|
||||
|
||||
private static func notificationPermissionGuidanceDialogSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
var deviceLanguage = ""
|
||||
var locale = ""
|
||||
|
||||
func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||
Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
|
||||
if waitForLoadingIndicator {
|
||||
Snapshot.snapshot(name)
|
||||
@@ -32,7 +33,6 @@ func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
|
||||
/// - Parameters:
|
||||
/// - name: The name of the snapshot
|
||||
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
|
||||
@MainActor
|
||||
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||
Snapshot.snapshot(name, timeWaitingForIdle: timeout)
|
||||
}
|
||||
@@ -52,7 +52,6 @@ enum SnapshotError: Error, CustomDebugStringConvertible {
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
@MainActor
|
||||
open class Snapshot: NSObject {
|
||||
static var app: XCUIApplication?
|
||||
static var waitForAnimations = true
|
||||
@@ -60,8 +59,6 @@ open class Snapshot: NSObject {
|
||||
static var screenshotsDirectory: URL? {
|
||||
return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
|
||||
}
|
||||
static var deviceLanguage = ""
|
||||
static var currentLocale = ""
|
||||
|
||||
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||
|
||||
@@ -106,17 +103,17 @@ open class Snapshot: NSObject {
|
||||
|
||||
do {
|
||||
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||
currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||
locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||
} catch {
|
||||
NSLog("Couldn't detect/set locale...")
|
||||
}
|
||||
|
||||
if currentLocale.isEmpty && !deviceLanguage.isEmpty {
|
||||
currentLocale = Locale(identifier: deviceLanguage).identifier
|
||||
if locale.isEmpty && !deviceLanguage.isEmpty {
|
||||
locale = Locale(identifier: deviceLanguage).identifier
|
||||
}
|
||||
|
||||
if !currentLocale.isEmpty {
|
||||
app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""]
|
||||
if !locale.isEmpty {
|
||||
app.launchArguments += ["-AppleLocale", "\"\(locale)\""]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +165,7 @@ open class Snapshot: NSObject {
|
||||
}
|
||||
|
||||
let screenshot = XCUIScreen.main.screenshot()
|
||||
#if os(iOS) && !targetEnvironment(macCatalyst)
|
||||
#if os(iOS)
|
||||
let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image
|
||||
#else
|
||||
let image = screenshot.image
|
||||
@@ -184,7 +181,7 @@ open class Snapshot: NSObject {
|
||||
|
||||
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
|
||||
#if swift(<5.0)
|
||||
try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
|
||||
UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
|
||||
#else
|
||||
try image.pngData()?.write(to: path, options: .atomic)
|
||||
#endif
|
||||
@@ -284,7 +281,6 @@ private extension XCUIElementQuery {
|
||||
return self.containing(isNetworkLoadingIndicator)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var deviceStatusBars: XCUIElementQuery {
|
||||
guard let app = Snapshot.app else {
|
||||
fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
@@ -310,4 +306,4 @@ private extension CGFloat {
|
||||
|
||||
// Please don't remove the lines below
|
||||
// They are used to detect outdated configuration files
|
||||
// SnapshotHelperVersion [1.30]
|
||||
// SnapshotHelperVersion [1.27]
|
||||
|
||||
@@ -10,24 +10,7 @@ default_platform(:ios)
|
||||
|
||||
APP_STORE_APP_IDENTIFIER = "ai.openclawfoundation.app"
|
||||
DEFAULT_APP_STORE_CONNECT_KEYCHAIN_SERVICE = "openclaw-app-store-connect-key"
|
||||
DEFAULT_SNAPSHOT_DEVICE_FAMILIES = [
|
||||
{
|
||||
label: "iPhone",
|
||||
patterns: [
|
||||
/\AiPhone .* Pro Max\z/,
|
||||
/\AiPhone .* Plus\z/,
|
||||
/\AiPhone .*\z/
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "13-inch iPad",
|
||||
patterns: [
|
||||
/\AiPad Pro 13-inch/,
|
||||
/\AiPad Air 13-inch/,
|
||||
/\AiPad .*13-inch/
|
||||
]
|
||||
}
|
||||
].freeze
|
||||
DEFAULT_SNAPSHOT_DEVICES = ["iPhone 16 Pro Max", "iPad Pro 13-inch (M4)"].freeze
|
||||
DEFAULT_WATCH_SNAPSHOT_DEVICE = "Apple Watch Ultra 3 (49mm)"
|
||||
WATCH_SCREENSHOT_MODE_DEFAULTS_KEY = "openclaw.watch.screenshotMode"
|
||||
WATCH_SNAPSHOT_STATUS_BAR_TIME = "09:41"
|
||||
@@ -94,23 +77,11 @@ end
|
||||
|
||||
def snapshot_devices
|
||||
raw = ENV["OPENCLAW_SNAPSHOT_DEVICES"].to_s.strip
|
||||
return default_snapshot_devices if raw.empty?
|
||||
return DEFAULT_SNAPSHOT_DEVICES if raw.empty?
|
||||
|
||||
raw.split(",").map(&:strip).reject(&:empty?)
|
||||
end
|
||||
|
||||
def default_snapshot_devices
|
||||
names = available_simulator_devices.map { |device| device["name"].to_s }.reject(&:empty?).uniq
|
||||
|
||||
DEFAULT_SNAPSHOT_DEVICE_FAMILIES.map do |family|
|
||||
match = family.fetch(:patterns).filter_map do |pattern|
|
||||
names.find { |name| name.match?(pattern) }
|
||||
end.first
|
||||
UI.user_error!("No available #{family.fetch(:label)} simulator found for App Store screenshots.") if match.nil?
|
||||
match
|
||||
end
|
||||
end
|
||||
|
||||
def watch_snapshot_device
|
||||
raw = ENV["OPENCLAW_WATCH_SNAPSHOT_DEVICE"].to_s.strip
|
||||
raw.empty? ? DEFAULT_WATCH_SNAPSHOT_DEVICE : raw
|
||||
@@ -142,51 +113,6 @@ def resolve_simulator_device(name)
|
||||
fallback
|
||||
end
|
||||
|
||||
def install_ready_for_review_edit_state_lookup!
|
||||
require "spaceship"
|
||||
|
||||
app_class = Spaceship::ConnectAPI::App
|
||||
app_class.class_eval do
|
||||
unless method_defined?(:openclaw_get_edit_app_store_version_without_ready_for_review)
|
||||
alias_method :openclaw_get_edit_app_store_version_without_ready_for_review, :get_edit_app_store_version
|
||||
end
|
||||
|
||||
unless method_defined?(:openclaw_fetch_edit_app_info_without_ready_for_review)
|
||||
alias_method :openclaw_fetch_edit_app_info_without_ready_for_review, :fetch_edit_app_info
|
||||
end
|
||||
|
||||
def get_edit_app_store_version(client: nil, platform: nil, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES)
|
||||
version = openclaw_get_edit_app_store_version_without_ready_for_review(client: client, platform: platform, includes: includes)
|
||||
return version if version
|
||||
|
||||
# First public releases can leave the only version in READY_FOR_REVIEW.
|
||||
# Fastlane 2.236.1 excludes that state and then tries to create an illegal
|
||||
# second version; use the existing review-ready version as the edit target.
|
||||
client ||= Spaceship::ConnectAPI
|
||||
platform ||= Spaceship::ConnectAPI::Platform::IOS
|
||||
filter = {
|
||||
appVersionState: Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::READY_FOR_REVIEW,
|
||||
platform: platform
|
||||
}
|
||||
|
||||
get_app_store_versions(client: client, filter: filter, includes: includes)
|
||||
.sort_by { |candidate| Gem::Version.new(candidate.version_string) }
|
||||
.last
|
||||
end
|
||||
|
||||
def fetch_edit_app_info(client: nil, includes: Spaceship::ConnectAPI::AppInfo::ESSENTIAL_INCLUDES)
|
||||
app_info = openclaw_fetch_edit_app_info_without_ready_for_review(client: client, includes: includes)
|
||||
return app_info if app_info
|
||||
|
||||
client ||= Spaceship::ConnectAPI
|
||||
client
|
||||
.get_app_infos(app_id: id, includes: includes)
|
||||
.to_models
|
||||
.find { |candidate| candidate.state == Spaceship::ConnectAPI::AppInfo::State::READY_FOR_REVIEW }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def bundle_identifier_for_product(product_path)
|
||||
info_plist_path = File.join(product_path, "Info.plist")
|
||||
UI.user_error!("Expected Info.plist at #{info_plist_path}.") unless File.exist?(info_plist_path)
|
||||
@@ -284,7 +210,6 @@ def normalize_watch_screenshot_status_bar(path)
|
||||
script = <<~SWIFT
|
||||
import AppKit
|
||||
import Foundation
|
||||
import ImageIO
|
||||
|
||||
let path = CommandLine.arguments[1]
|
||||
let timeText = CommandLine.arguments[2]
|
||||
@@ -296,37 +221,36 @@ def normalize_watch_screenshot_status_bar(path)
|
||||
exit(2)
|
||||
}
|
||||
|
||||
let width = cgImage.width
|
||||
let height = cgImage.height
|
||||
let drawWidth = CGFloat(width)
|
||||
let drawHeight = CGFloat(height)
|
||||
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
|
||||
guard let bitmapContext = CGContext(
|
||||
data: nil,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: width * 4,
|
||||
space: colorSpace,
|
||||
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
|
||||
let width = CGFloat(cgImage.width)
|
||||
let height = CGFloat(cgImage.height)
|
||||
guard let bitmap = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil,
|
||||
pixelsWide: Int(width),
|
||||
pixelsHigh: Int(height),
|
||||
bitsPerSample: 8,
|
||||
samplesPerPixel: 4,
|
||||
hasAlpha: true,
|
||||
isPlanar: false,
|
||||
colorSpaceName: .deviceRGB,
|
||||
bytesPerRow: 0,
|
||||
bitsPerPixel: 0),
|
||||
let graphicsContext = NSGraphicsContext(bitmapImageRep: bitmap)
|
||||
else {
|
||||
fputs("Failed to create normalized screenshot bitmap at \\(path)\\n", stderr)
|
||||
exit(3)
|
||||
}
|
||||
|
||||
let graphicsContext = NSGraphicsContext(cgContext: bitmapContext, flipped: false)
|
||||
bitmap.size = NSSize(width: width, height: height)
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = graphicsContext
|
||||
NSColor.black.setFill()
|
||||
NSBezierPath(rect: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight)).fill()
|
||||
source.draw(
|
||||
in: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight),
|
||||
from: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight),
|
||||
operation: .sourceOver,
|
||||
in: NSRect(x: 0, y: 0, width: width, height: height),
|
||||
from: NSRect(x: 0, y: 0, width: width, height: height),
|
||||
operation: .copy,
|
||||
fraction: 1.0)
|
||||
|
||||
NSColor.black.setFill()
|
||||
NSBezierPath(rect: NSRect(x: drawWidth - 146, y: drawHeight - 92, width: 124, height: 70)).fill()
|
||||
NSBezierPath(rect: NSRect(x: width - 146, y: height - 92, width: 124, height: 70)).fill()
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = .right
|
||||
@@ -336,26 +260,17 @@ def normalize_watch_screenshot_status_bar(path)
|
||||
.paragraphStyle: paragraphStyle,
|
||||
]
|
||||
timeText.draw(
|
||||
in: NSRect(x: drawWidth - 134, y: drawHeight - 82, width: 102, height: 44),
|
||||
in: NSRect(x: width - 134, y: height - 82, width: 102, height: 44),
|
||||
withAttributes: attributes)
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
guard let output = bitmapContext.makeImage(),
|
||||
let destination = CGImageDestinationCreateWithURL(
|
||||
URL(fileURLWithPath: path) as CFURL,
|
||||
"public.png" as CFString,
|
||||
1,
|
||||
nil)
|
||||
guard let png = bitmap.representation(using: .png, properties: [:])
|
||||
else {
|
||||
fputs("Failed to encode normalized screenshot at \\(path)\\n", stderr)
|
||||
exit(4)
|
||||
}
|
||||
|
||||
CGImageDestinationAddImage(destination, output, nil)
|
||||
guard CGImageDestinationFinalize(destination) else {
|
||||
fputs("Failed to write normalized screenshot at \\(path)\\n", stderr)
|
||||
exit(5)
|
||||
}
|
||||
try png.write(to: URL(fileURLWithPath: path))
|
||||
SWIFT
|
||||
|
||||
Tempfile.create(["openclaw-watch-status-bar", ".swift"]) do |file|
|
||||
@@ -1046,7 +961,6 @@ platform :ios do
|
||||
|
||||
desc "Upload App Store metadata (and optionally screenshots)"
|
||||
lane :metadata do
|
||||
install_ready_for_review_edit_state_lookup!
|
||||
sync_ios_versioning!
|
||||
version_metadata = read_ios_version_metadata
|
||||
api_key = app_store_connect_api_key_config
|
||||
|
||||
@@ -104,7 +104,7 @@ Generate deterministic App Store screenshots:
|
||||
pnpm ios:screenshots
|
||||
```
|
||||
|
||||
The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters the built-in connected screenshot fixture instead of pairing with a live gateway. By default it chooses one available large iPhone simulator and one available 13-inch iPad simulator from the installed Xcode runtime; override devices with a comma-separated `OPENCLAW_SNAPSHOT_DEVICES` value when the requested simulators exist locally.
|
||||
The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters the built-in connected screenshot fixture instead of pairing with a live gateway. By default it captures the tab set on `iPhone 16 Pro Max` and `iPad Pro 13-inch (M4)`; override devices with a comma-separated `OPENCLAW_SNAPSHOT_DEVICES` value when the requested simulators exist locally.
|
||||
|
||||
Upload to App Store Connect:
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ project("OpenClaw.xcodeproj")
|
||||
scheme("OpenClawUITests")
|
||||
configuration("Debug")
|
||||
|
||||
# The Fastfile screenshot lane resolves concrete device names from the installed
|
||||
# Xcode simulators. Fastlane validates Snapfile devices before lane overrides, so
|
||||
# this file intentionally does not hardcode simulator model names.
|
||||
devices([
|
||||
"iPhone 16 Pro Max",
|
||||
"iPad Pro 13-inch (M4)",
|
||||
])
|
||||
|
||||
languages([
|
||||
"en-US",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Bundled A2UI runtime resource embedded by OpenClawKit.
|
||||
var __defProp$1 = Object.defineProperty;
|
||||
var __exportAll = (all, no_symbols) => {
|
||||
let target = {};
|
||||
@@ -11935,10 +11936,6 @@ var __runInitializers = function(thisArg, initializers, value) {
|
||||
};
|
||||
return _classThis;
|
||||
})();
|
||||
/**
|
||||
* Canvas A2UI browser bootstrap that installs theme overrides and native bridge
|
||||
* helpers.
|
||||
*/
|
||||
const modalStyles = i$10`
|
||||
dialog {
|
||||
position: fixed;
|
||||
|
||||
@@ -155,7 +155,6 @@ Notes:
|
||||
|
||||
- `onchar` still responds to explicit @mentions.
|
||||
- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred.
|
||||
- After the bot sends a visible reply in a channel thread, later messages in that same thread are answered without a new @mention or `onchar` prefix, so multi-turn thread conversations keep flowing. Participation is remembered for 7 days of thread inactivity (refreshed on each reply) and persists across gateway restarts. Threads the bot has only observed are unaffected; start a new top-level message to require an explicit mention again.
|
||||
|
||||
## Threading and sessions
|
||||
|
||||
|
||||
@@ -151,7 +151,6 @@ to a group, then mention it or configure the group to run without a mention.
|
||||
groups: {
|
||||
"*": {
|
||||
requireMention: true,
|
||||
commandLevel: "all",
|
||||
historyLimit: 50,
|
||||
tools: { deny: ["exec", "read", "write"] },
|
||||
},
|
||||
@@ -159,7 +158,6 @@ to a group, then mention it or configure the group to run without a mention.
|
||||
name: "Release room",
|
||||
requireMention: false,
|
||||
ignoreOtherMentions: true,
|
||||
commandLevel: "safety",
|
||||
historyLimit: 20,
|
||||
prompt: "Keep replies short and operational.",
|
||||
},
|
||||
@@ -174,9 +172,6 @@ to a group, then mention it or configure the group to run without a mention.
|
||||
settings include:
|
||||
|
||||
- `requireMention`: require an @mention before the bot replies. Default: `true`.
|
||||
- `commandLevel`: control which built-in slash commands can run in groups.
|
||||
Default: `all`, which preserves the pre-existing QQBot group behavior when the
|
||||
setting is omitted.
|
||||
- `ignoreOtherMentions`: drop messages that mention someone else but not the bot.
|
||||
- `historyLimit`: keep recent non-mention group messages as context for the next mentioned turn. Set `0` to disable.
|
||||
- `tools`: allow/deny tools for the whole group.
|
||||
@@ -184,17 +179,6 @@ settings include:
|
||||
- `name`: friendly label used in logs and group context.
|
||||
- `prompt`: per-group behavior prompt appended to the agent context.
|
||||
|
||||
`commandLevel` accepts:
|
||||
|
||||
- `all`: keep recognized built-in commands available as before. Some commands may
|
||||
stay hidden from menus, but authorized users can still run them in the group.
|
||||
- `safety`: allow common collaboration commands such as `/help`, `/btw`, and
|
||||
`/stop`; ask users to run sensitive commands such as `/config`, `/tools`, and
|
||||
`/bash` in private chat.
|
||||
- `strict`: only allow the group-session controls needed for strict group
|
||||
operation. `/stop` still stays urgent so an authorized sender can interrupt an
|
||||
active run.
|
||||
|
||||
Old QQBot `toolPolicy` entries are retired. Run `openclaw doctor --fix` to migrate them to `tools`.
|
||||
|
||||
Activation modes are `mention` and `always`. `requireMention: true` maps to
|
||||
|
||||
@@ -198,7 +198,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured
|
||||
|
||||
## Full Release Validation
|
||||
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, maturity scorecard rendering from QA profile evidence, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
|
||||
See [Full release validation](/reference/full-release-validation) for the
|
||||
stage matrix, exact workflow job names, profile differences, artifacts, and
|
||||
|
||||
@@ -154,11 +154,6 @@ openclaw skills workshop reject <proposal-id> --reason "Duplicate"
|
||||
openclaw skills workshop quarantine <proposal-id> --reason "Needs security review"
|
||||
```
|
||||
|
||||
`propose-create` always targets a new sibling skill under `skills/<name>/` and
|
||||
rejects drafts that reference existing workspace skill paths such as
|
||||
`skills/qa-check/SKILL.md`. Use `propose-update <skill>` once per existing skill
|
||||
when a change touches current skills.
|
||||
|
||||
## Related
|
||||
|
||||
- [CLI reference](/cli)
|
||||
|
||||
@@ -68,14 +68,6 @@
|
||||
"source": "/reference/openclaw-sdk-api-design",
|
||||
"destination": "/gateway/external-apps"
|
||||
},
|
||||
{
|
||||
"source": "/reference/maturity-scorecard",
|
||||
"destination": "/maturity/scorecard"
|
||||
},
|
||||
{
|
||||
"source": "/reference/maturity-taxonomy",
|
||||
"destination": "/maturity/taxonomy"
|
||||
},
|
||||
{
|
||||
"source": "/mcp",
|
||||
"destination": "/cli/mcp"
|
||||
@@ -1860,8 +1852,6 @@
|
||||
{
|
||||
"group": "Release and CI",
|
||||
"pages": [
|
||||
"maturity/scorecard",
|
||||
"maturity/taxonomy",
|
||||
"reference/RELEASING",
|
||||
"reference/full-release-validation",
|
||||
"reference/release-performance-sweep",
|
||||
|
||||
@@ -110,18 +110,14 @@ systemctl --user daemon-reload
|
||||
### Windows (Scheduled Task)
|
||||
|
||||
Default task name is `OpenClaw Gateway` (or `OpenClaw Gateway (<profile>)`).
|
||||
The task script lives under your state dir as `gateway.cmd`; current installs may
|
||||
also create a windowless `gateway.vbs` launcher that Task Scheduler runs instead
|
||||
of opening `gateway.cmd` directly.
|
||||
The task script lives under your state dir.
|
||||
|
||||
```powershell
|
||||
schtasks /Delete /F /TN "OpenClaw Gateway"
|
||||
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd" -ErrorAction SilentlyContinue
|
||||
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.vbs" -ErrorAction SilentlyContinue
|
||||
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd"
|
||||
```
|
||||
|
||||
If you used a profile, delete the matching task name and the `gateway.cmd` /
|
||||
`gateway.vbs` files under `~\.openclaw-<profile>`.
|
||||
If you used a profile, delete the matching task name and `~\.openclaw-<profile>\gateway.cmd`.
|
||||
|
||||
## Normal install vs source checkout
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -124,11 +124,8 @@ openclaw gateway status --json
|
||||
```
|
||||
|
||||
Native Windows CLI and Gateway flows are supported and continue to improve.
|
||||
Managed startup uses Windows Scheduled Tasks when available. The task keeps the
|
||||
readable `gateway.cmd` script in the OpenClaw state dir, but launches it through
|
||||
a generated `gateway.vbs` WScript wrapper so the background Gateway does not open
|
||||
a visible console window. If task creation is denied, OpenClaw falls back to a
|
||||
per-user Startup-folder login item.
|
||||
Managed startup uses Windows Scheduled Tasks when available and falls back to a
|
||||
per-user Startup-folder login item if task creation is denied.
|
||||
|
||||
To install the Gateway service:
|
||||
|
||||
|
||||
@@ -259,10 +259,14 @@ under `describe("runSideQuestion")`.
|
||||
fallback for whatever runtimes do not have a peer surface.
|
||||
- PI session state is not migrated when an agent switches to `copilot`.
|
||||
Selection is per attempt; existing PI sessions remain valid.
|
||||
- `ask_user` uses the same OpenClaw prompt-and-reply path as the Codex
|
||||
harness. When the Copilot SDK asks for user input, OpenClaw posts a
|
||||
blocking prompt to the active channel/TUI and the next queued user
|
||||
message resolves the SDK request.
|
||||
- **Interactive `ask_user` is not yet wired.** The SDK's
|
||||
`onUserInputRequest` handler is intentionally not registered, which
|
||||
per the SDK contract hides the `ask_user` tool from the model
|
||||
entirely. Agents running under this harness make best-judgment
|
||||
decisions from the initial prompt rather than asking clarifying
|
||||
questions mid-turn. A follow-up will port the codex pattern at
|
||||
`extensions/codex/src/app-server/user-input-bridge.ts` to route SDK
|
||||
`UserInputRequest`s through the OpenClaw channel/TUI prompt path.
|
||||
|
||||
## Permissions and ask_user
|
||||
|
||||
@@ -324,15 +328,11 @@ the tool bridge. The bridge also forwards the bounded tool-construction
|
||||
controls it can enforce at the SDK boundary: `includeCoreTools`, the
|
||||
runtime tool allowlist, and `toolConstructionPlan`.
|
||||
|
||||
The bridge also uses the shared harness tool-surface helper from
|
||||
`openclaw/plugin-sdk/agent-harness-tool-runtime` for PI parity. When
|
||||
tool-search is enabled, the SDK sees compact control tools plus a hidden
|
||||
catalog executor instead of every OpenClaw tool schema. When code mode is
|
||||
enabled, the helper builds the same code-mode control surface and catalog
|
||||
lifecycle used by other agent harnesses. Local-model lean defaults,
|
||||
runtime-compatible schema filtering, directory hydration, and catalog
|
||||
cleanup all stay in the shared helper so Copilot and Codex-adjacent
|
||||
harnesses do not drift.
|
||||
The remaining PI tool-search/code-mode fields are intentionally **not**
|
||||
forwarded at MVP and tracked as follow-ups: `toolSearchCatalogRef`,
|
||||
`includeToolSearchControls`, and `toolSearchCatalogExecutor`. Those
|
||||
controls drive PI's native tool-search UI and have no direct Copilot SDK
|
||||
analog yet.
|
||||
|
||||
### Session-level GitHub token
|
||||
|
||||
@@ -349,10 +349,7 @@ When the resolved mode is `useLoggedInUser`, the session-level field
|
||||
is omitted so the SDK keeps deriving identity from the logged-in
|
||||
identity.
|
||||
|
||||
`ask_user` uses `SessionConfig.onUserInputRequest`. The bridge accepts
|
||||
choice indexes or labels for fixed-choice requests, accepts free-form
|
||||
answers when the SDK request allows them, and cancels a pending request
|
||||
when the OpenClaw attempt is aborted.
|
||||
`ask_user` is intentionally hidden — see Limitations above.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -46,8 +46,10 @@ The default model is `embeddinggemma-300m-qat-Q8_0.gguf`. You can also point
|
||||
|
||||
## Native Runtime
|
||||
|
||||
Use Node 24 for the smoothest native install path. Source checkouts using pnpm
|
||||
may need to approve and rebuild the native dependency:
|
||||
Use Node 24 for the smoothest native install path. Source checkouts leave the
|
||||
native build unapproved by default so normal workspace installs do not compile
|
||||
llama.cpp. When you actually use local GGUF embeddings from source, approve and
|
||||
rebuild the provider runtime:
|
||||
|
||||
```bash
|
||||
pnpm approve-builds
|
||||
|
||||
@@ -196,23 +196,6 @@ finish. Both helpers accept the same `{ event, ctx }` payload as
|
||||
`runAgentHarnessAgentEndHook(...)`; their failures do not alter the completed
|
||||
attempt result.
|
||||
|
||||
### User input and tool surfaces
|
||||
|
||||
Native harnesses that expose a runtime-level user-input request should use the
|
||||
user-input helpers from `openclaw/plugin-sdk/agent-harness-runtime` to format
|
||||
the prompt, deliver it through OpenClaw's blocking reply path, and normalize
|
||||
choice/free-form answers back into the runtime's native response shape. The
|
||||
helper keeps channel/TUI presentation consistent while each harness keeps its
|
||||
own protocol parsing and pending-request lifecycle.
|
||||
|
||||
Native harnesses that need PI-like compact tool routing should use
|
||||
`createAgentHarnessToolSurfaceRuntime(...)` from
|
||||
`openclaw/plugin-sdk/agent-harness-tool-runtime`. It owns
|
||||
tool-search/code-mode control selection, local-model lean defaults,
|
||||
runtime-compatible schema filtering, hidden catalog execution, directory
|
||||
hydration, and catalog cleanup. Harnesses still own their SDK-specific tool
|
||||
conversion and native execution callback.
|
||||
|
||||
### Native Codex harness mode
|
||||
|
||||
The bundled `codex` harness is the native Codex mode for embedded OpenClaw
|
||||
|
||||
@@ -84,8 +84,8 @@ Choose the Token Plan auth choice that matches the regional base URL shown in Xi
|
||||
|
||||
| Model ref | Input | Context | Max output | Reasoning | Notes |
|
||||
| --------------------------------- | ----------- | --------- | ---------- | --------- | ------------- |
|
||||
| `xiaomi-token-plan/mimo-v2.5-pro` | text | 1,048,576 | 131,072 | Yes | Default model |
|
||||
| `xiaomi-token-plan/mimo-v2.5` | text, image | 1,048,576 | 131,072 | Yes | Multimodal |
|
||||
| `xiaomi-token-plan/mimo-v2.5-pro` | text | 1,048,576 | 32,000 | Yes | Default model |
|
||||
| `xiaomi-token-plan/mimo-v2.5` | text, image | 1,048,576 | 32,000 | Yes | Multimodal |
|
||||
|
||||
<Tip>
|
||||
Token Plan onboarding validates the key shape and warns when a `tp-...` key is entered into the pay-as-you-go path, or an `sk-...` key is entered into the Token Plan path.
|
||||
@@ -222,7 +222,7 @@ Token Plan:
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 131072,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "mimo-v2.5",
|
||||
@@ -230,7 +230,7 @@ Token Plan:
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 131072,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
750
docs/style.css
750
docs/style.css
@@ -135,753 +135,3 @@ html.dark .nav-tabs-underline {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.maturity-hero {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin: 10px 0 38px;
|
||||
padding: 4px 0 26px 20px;
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 22%, transparent);
|
||||
border-left: 3px solid rgb(var(--primary));
|
||||
}
|
||||
|
||||
.maturity-hero-compact {
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
.maturity-hero h2 {
|
||||
max-width: 46rem;
|
||||
margin: 0;
|
||||
font-size: clamp(26px, 3vw, 38px);
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.maturity-hero-title {
|
||||
max-width: 46rem;
|
||||
margin: 0;
|
||||
font-size: clamp(26px, 3vw, 38px);
|
||||
font-weight: 750;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.maturity-hero > p:not(.maturity-kicker):not(.maturity-jump-links) {
|
||||
max-width: 58rem;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
opacity: 0.76;
|
||||
}
|
||||
|
||||
.maturity-kicker {
|
||||
margin: 0;
|
||||
color: rgb(var(--primary));
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 1.3;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.maturity-jump-links {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.maturity-jump-links a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.maturity-jump-links a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.maturity-score-stable,
|
||||
.maturity-band-stable {
|
||||
color: #4ca574;
|
||||
}
|
||||
|
||||
.maturity-score-beta,
|
||||
.maturity-band-beta {
|
||||
color: #849fd2;
|
||||
}
|
||||
|
||||
.maturity-score-alpha,
|
||||
.maturity-band-alpha {
|
||||
color: #d39a4b;
|
||||
}
|
||||
|
||||
.maturity-score-experimental,
|
||||
.maturity-band-experimental {
|
||||
color: #dc7669;
|
||||
}
|
||||
|
||||
.maturity-score-clawesome,
|
||||
.maturity-band-clawesome {
|
||||
color: #46b59a;
|
||||
}
|
||||
|
||||
.maturity-level-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid color-mix(in oklab, currentColor 32%, transparent);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklab, currentColor 10%, transparent);
|
||||
color: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.maturity-level-code {
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.maturity-level-experimental {
|
||||
color: #dc7669;
|
||||
}
|
||||
|
||||
.maturity-level-alpha {
|
||||
color: #d39a4b;
|
||||
}
|
||||
|
||||
.maturity-level-beta {
|
||||
color: #849fd2;
|
||||
}
|
||||
|
||||
.maturity-level-stable {
|
||||
color: #4ca574;
|
||||
}
|
||||
|
||||
.maturity-level-clawesome {
|
||||
color: #46b59a;
|
||||
}
|
||||
|
||||
.maturity-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin: 14px 0 20px;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
|
||||
}
|
||||
|
||||
.maturity-summary-item {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 18px 20px 18px 0;
|
||||
}
|
||||
|
||||
.maturity-summary-item + .maturity-summary-item {
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
|
||||
}
|
||||
|
||||
.maturity-summary-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.maturity-summary-value {
|
||||
display: inline-block;
|
||||
font-size: 30px;
|
||||
font-weight: 750;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.maturity-summary-heading > span:not(.maturity-summary-value) {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-summary-bar {
|
||||
height: 7px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in oklab, currentColor 14%, transparent);
|
||||
}
|
||||
|
||||
.maturity-summary-bar span {
|
||||
display: block;
|
||||
width: calc(var(--score) * 1%);
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.maturity-summary-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.maturity-summary-meta span:first-child {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-summary-meta span:last-child {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.maturity-band-list {
|
||||
display: flex;
|
||||
margin: 12px 0 30px;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
|
||||
}
|
||||
|
||||
.maturity-band {
|
||||
display: grid;
|
||||
flex: 1 1 0;
|
||||
gap: 3px;
|
||||
padding: 10px 12px 11px 0;
|
||||
}
|
||||
|
||||
.maturity-band + .maturity-band {
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
|
||||
}
|
||||
|
||||
.maturity-band-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-band-title + span {
|
||||
color: inherit;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.maturity-band > span:last-child {
|
||||
color: inherit;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.maturity-band span {
|
||||
color: inherit;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.maturity-band .maturity-level-pill {
|
||||
font-size: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.maturity-band .maturity-level-pill span {
|
||||
font-size: inherit;
|
||||
opacity: inherit;
|
||||
}
|
||||
|
||||
.maturity-score {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
color: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-score-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.maturity-score-label > span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.maturity-score-label > span:last-child {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.maturity-score-label .maturity-level-pill {
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.maturity-score-label-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.maturity-summary-meta .maturity-level-pill {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.maturity-meter {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in oklab, currentColor 15%, transparent);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.maturity-meter > span {
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.maturity-score-unscored,
|
||||
.maturity-lts-none {
|
||||
color: inherit;
|
||||
opacity: 0.52;
|
||||
}
|
||||
|
||||
.maturity-surface-table {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin: 8px 0 22px;
|
||||
}
|
||||
|
||||
.maturity-surface-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(190px, 1.55fr) repeat(3, minmax(110px, 1fr)) minmax(72px, 0.55fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 13px 0;
|
||||
border-top: 1px solid color-mix(in oklab, currentColor 14%, transparent);
|
||||
}
|
||||
|
||||
.maturity-surface-row-header {
|
||||
padding: 0 0 9px;
|
||||
border-top: 0;
|
||||
color: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.56;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.maturity-surface-name {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
border-bottom: 0 !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.maturity-surface-name:hover .maturity-surface-title {
|
||||
color: rgb(var(--primary));
|
||||
}
|
||||
|
||||
.maturity-surface-title {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.maturity-surface-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.maturity-surface-meta > span:not(.maturity-level-pill) {
|
||||
font-size: 11px;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.maturity-surface-meta .maturity-level-pill {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.maturity-surface-metric {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.maturity-surface-metric-label {
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.maturity-surface-support {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.maturity-lts {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.maturity-lts::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.maturity-lts-partial {
|
||||
color: #d39a4b;
|
||||
}
|
||||
|
||||
.maturity-lts-full {
|
||||
color: #4ca574;
|
||||
}
|
||||
|
||||
.maturity-evidence-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin: 14px 0 24px;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
|
||||
}
|
||||
|
||||
.maturity-evidence-card {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: 14px 16px 14px 0;
|
||||
}
|
||||
|
||||
.maturity-evidence-card + .maturity-evidence-card {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
|
||||
}
|
||||
|
||||
.maturity-evidence-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-evidence-card span {
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.maturity-readiness-summary {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.maturity-readiness-list {
|
||||
display: grid;
|
||||
margin: 0;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
|
||||
}
|
||||
|
||||
.maturity-readiness-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(120px, 0.8fr) minmax(110px, 0.7fr);
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
padding: 11px 0;
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 10%, transparent);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.maturity-readiness-row-header {
|
||||
padding: 8px 0;
|
||||
border-bottom-color: color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.56;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.maturity-readiness-area {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.maturity-readiness-title {
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-readiness-status {
|
||||
font-size: 10px;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.maturity-readiness-status-ready {
|
||||
color: #4ca574;
|
||||
}
|
||||
|
||||
.maturity-readiness-status-partially-reviewed {
|
||||
color: #d39a4b;
|
||||
}
|
||||
|
||||
.maturity-readiness-status-needs-review {
|
||||
color: #dc7669;
|
||||
}
|
||||
|
||||
.maturity-category-list {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.maturity-category-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1.55fr) repeat(3, minmax(100px, 1fr)) minmax(140px, 1.2fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.maturity-category-row-header {
|
||||
padding: 8px 0;
|
||||
border-top: 0;
|
||||
color: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.56;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.maturity-category-area {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.maturity-category-title {
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-category-area > span:last-child {
|
||||
font-size: 10px;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.maturity-category-docs {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.maturity-category-docs a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.maturity-category-docs a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.maturity-level-list {
|
||||
display: grid;
|
||||
margin: 12px 0 28px;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
|
||||
}
|
||||
|
||||
.maturity-level-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 0.32fr) minmax(0, 1fr);
|
||||
gap: 4px 14px;
|
||||
padding: 13px 0;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
|
||||
}
|
||||
|
||||
.maturity-level-row:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.maturity-level-title {
|
||||
grid-row: span 2;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-level-title .maturity-level-pill {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.maturity-level-row span,
|
||||
.maturity-level-promotion {
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.maturity-surface-link {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
margin: 0;
|
||||
padding: 11px 0;
|
||||
border-bottom: 1px solid color-mix(in oklab, currentColor 14%, transparent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.maturity-surface-link:hover {
|
||||
color: rgb(var(--primary));
|
||||
}
|
||||
|
||||
.maturity-surface-link .maturity-surface-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maturity-surface-link > .maturity-surface-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.maturity-surface-link > .maturity-surface-meta > span:not(.maturity-level-pill) {
|
||||
font-size: 11px;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.maturity-surface-link > .maturity-surface-meta .maturity-level-pill {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.maturity-surface-rollup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px 14px;
|
||||
margin: 0 0 14px;
|
||||
padding: 9px 0;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
|
||||
}
|
||||
|
||||
.maturity-surface-rollup > span {
|
||||
color: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#content table .maturity-score,
|
||||
#content table .maturity-lts {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.maturity-summary-grid,
|
||||
.maturity-evidence-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.maturity-summary-item,
|
||||
.maturity-summary-item + .maturity-summary-item,
|
||||
.maturity-evidence-card,
|
||||
.maturity-evidence-card + .maturity-evidence-card {
|
||||
padding: 14px 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.maturity-summary-item + .maturity-summary-item,
|
||||
.maturity-evidence-card + .maturity-evidence-card {
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
|
||||
}
|
||||
|
||||
.maturity-surface-row {
|
||||
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(98px, 1fr)) minmax(70px, 0.5fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.maturity-category-row {
|
||||
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(86px, 1fr)) minmax(110px, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.maturity-hero {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.maturity-surface-row-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.maturity-surface-row {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 9px 12px;
|
||||
}
|
||||
|
||||
.maturity-surface-metric {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.maturity-surface-metric-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.maturity-surface-support {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.maturity-readiness-row,
|
||||
.maturity-category-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.maturity-readiness-row-header,
|
||||
.maturity-category-row-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.maturity-category-docs {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.maturity-band-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.maturity-band + .maturity-band {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.maturity-level-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.maturity-level-title {
|
||||
grid-row: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +88,8 @@ still returns one synthesized answer with citations rather than an N-result
|
||||
list.
|
||||
|
||||
`freshness` accepts `day`, `week`, `month`, `year`, and the shared shortcuts
|
||||
`pd`, `pw`, `pm`, and `py`. `day`/`pd` adds a recency instruction to the Gemini
|
||||
query instead of a hard 24-hour range. `week`, `month`, `year`, and explicit
|
||||
`date_after`/`date_before` ranges set Gemini Google Search grounding's
|
||||
`pd`, `pw`, `pm`, and `py`. OpenClaw converts these values, or an explicit
|
||||
`date_after`/`date_before` range, into Gemini Google Search grounding's
|
||||
`timeRangeFilter`. `country`, `language`, and `domain_filter` are not supported.
|
||||
|
||||
## Model selection
|
||||
|
||||
@@ -27,13 +27,7 @@ plugin, ClawHub, extra-root, managed, personal-agent, or system skills.
|
||||
active skills.
|
||||
- **Workspace scoped:** creates target the workspace `skills/` root. Updates
|
||||
are allowed only for writable workspace skills.
|
||||
- **Single target:** create always proposes one new sibling skill, and update
|
||||
targets one existing skill. Split multi-skill changes into separate update
|
||||
proposals.
|
||||
- **No clobber:** create fails if the target skill already exists.
|
||||
- **Wrong-target guard:** create rejects proposal content that references
|
||||
existing workspace skill paths such as `skills/trip-planning/SKILL.md`;
|
||||
those changes belong in update proposals.
|
||||
- **Hash bound:** update proposals bind to the current target hash and become
|
||||
stale if the live skill changes before apply.
|
||||
- **Scanner gated:** apply reruns scanning before writing.
|
||||
@@ -94,20 +88,12 @@ openclaw skills workshop propose-create \
|
||||
--proposal ./PROPOSAL.md
|
||||
```
|
||||
|
||||
`propose-create` always creates a new sibling under `skills/<name>/`. If the
|
||||
draft references an existing workspace skill path such as
|
||||
`skills/trip-planning/SKILL.md`, Skill Workshop rejects it so the update does
|
||||
not silently land in an unused sibling skill.
|
||||
|
||||
Create an update proposal for an existing workspace skill:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop propose-update trip-planning --proposal ./PROPOSAL.md
|
||||
```
|
||||
|
||||
For changes that touch multiple existing skills, create one update proposal per
|
||||
target skill.
|
||||
|
||||
List and inspect:
|
||||
|
||||
```bash
|
||||
@@ -181,10 +167,6 @@ The model uses `skill_workshop`:
|
||||
action: create | update | revise | list | inspect | apply | reject | quarantine
|
||||
```
|
||||
|
||||
`action=create` is only for a brand-new workspace skill. `action=update` targets
|
||||
one existing skill through `skill_name`. Agents should split multi-skill patches
|
||||
into separate `action=update` calls before applying them.
|
||||
|
||||
Agents must use `skill_workshop` for generated skill work. They must not create
|
||||
or change proposal files through `write`, `edit`, `exec`, shell commands, or
|
||||
direct filesystem operations.
|
||||
|
||||
@@ -389,8 +389,8 @@ show the `x_search` prompt.
|
||||
freshness ranges require both start and end dates.
|
||||
Gemini, Grok, and Kimi return one synthesized answer with citations. They
|
||||
accept `count` for shared-tool compatibility, but it does not change the
|
||||
grounded answer shape. Gemini treats `day` freshness as a recency hint; wider
|
||||
freshness values and explicit dates set Google Search grounding time ranges.
|
||||
grounded answer shape. Gemini supports `freshness`, `date_after`, and
|
||||
`date_before` by converting them to Google Search grounding time ranges.
|
||||
Perplexity behaves the same way when you use the Sonar/OpenRouter
|
||||
compatibility path (`plugins.entries.perplexity.config.webSearch.baseUrl` /
|
||||
`model` or `OPENROUTER_API_KEY`).
|
||||
|
||||
7
extensions/acpx/npm-shrinkwrap.json
generated
7
extensions/acpx/npm-shrinkwrap.json
generated
@@ -701,7 +701,6 @@
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.15.0.tgz",
|
||||
"integrity": "sha512-eAv7sGBeiYrYkOulF729nrM51szS7WIhBtugRj5wWq6csRKZUhAZfoUZlF8xUWdHPtOIzd/eT6MNG6gMHu6z0w==",
|
||||
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"codex-acp": "bin/codex-acp.js"
|
||||
@@ -722,7 +721,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -739,7 +737,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -756,7 +753,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -773,7 +769,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -790,7 +785,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -807,7 +801,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -15,12 +15,8 @@ import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const rootDir = path.resolve(pluginDir, "../..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const hashFile =
|
||||
process.env.OPENCLAW_A2UI_BUNDLE_HASH_FILE ??
|
||||
path.join(pluginDir, "src", "host", "a2ui", ".bundle.hash");
|
||||
const outputFile =
|
||||
process.env.OPENCLAW_A2UI_BUNDLE_OUT ??
|
||||
path.join(pluginDir, "src", "host", "a2ui", "a2ui.bundle.js");
|
||||
const hashFile = path.join(pluginDir, "src", "host", "a2ui", ".bundle.hash");
|
||||
const outputFile = path.join(pluginDir, "src", "host", "a2ui", "a2ui.bundle.js");
|
||||
const a2uiAppDir = path.join(pluginDir, "src", "host", "a2ui-app");
|
||||
const repoInputPaths = getBundleHashRepoInputPaths(rootDir);
|
||||
const relativeRepoInputPaths = repoInputPaths.map((inputPath) =>
|
||||
|
||||
@@ -11,9 +11,7 @@ const repoRoot = path.resolve(here, "../../../../..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const uiRoot = path.resolve(repoRoot, "ui");
|
||||
const fromHere = (p) => path.resolve(here, p);
|
||||
const outputFile = process.env.OPENCLAW_A2UI_BUNDLE_OUT
|
||||
? path.resolve(process.env.OPENCLAW_A2UI_BUNDLE_OUT)
|
||||
: path.resolve(here, "..", "a2ui", "a2ui.bundle.js");
|
||||
const outputFile = path.resolve(here, "..", "a2ui", "a2ui.bundle.js");
|
||||
|
||||
const a2uiLitIndex = require.resolve("@a2ui/lit");
|
||||
const a2uiLitUi = require.resolve("@a2ui/lit/ui");
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
* turns replies into app-server answer payloads.
|
||||
*/
|
||||
import {
|
||||
buildAgentHarnessUserInputAnswers,
|
||||
deliverAgentHarnessUserInputPrompt,
|
||||
embeddedAgentLog,
|
||||
emptyAgentHarnessUserInputAnswers,
|
||||
type AgentHarnessUserInputOption,
|
||||
type AgentHarnessUserInputQuestion,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { formatCodexDisplayText } from "../command-formatters.js";
|
||||
@@ -24,11 +19,25 @@ type PendingUserInput = {
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
questions: AgentHarnessUserInputQuestion[];
|
||||
questions: UserInputQuestion[];
|
||||
resolve: (value: JsonValue) => void;
|
||||
cleanup: () => void;
|
||||
};
|
||||
|
||||
type UserInputQuestion = {
|
||||
id: string;
|
||||
header: string;
|
||||
question: string;
|
||||
isOther: boolean;
|
||||
isSecret: boolean;
|
||||
options: UserInputOption[] | null;
|
||||
};
|
||||
|
||||
type UserInputOption = {
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type CodexUserInputBridge = {
|
||||
handleRequest: (request: {
|
||||
id: number | string;
|
||||
@@ -133,7 +142,7 @@ function readUserInputParams(value: JsonValue | undefined):
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
questions: AgentHarnessUserInputQuestion[];
|
||||
questions: UserInputQuestion[];
|
||||
}
|
||||
| undefined {
|
||||
if (!isJsonObject(value)) {
|
||||
@@ -148,11 +157,11 @@ function readUserInputParams(value: JsonValue | undefined):
|
||||
}
|
||||
const questions = questionsRaw
|
||||
.map(readQuestion)
|
||||
.filter((question): question is AgentHarnessUserInputQuestion => Boolean(question));
|
||||
.filter((question): question is UserInputQuestion => Boolean(question));
|
||||
return { threadId, turnId, itemId, questions };
|
||||
}
|
||||
|
||||
function readQuestion(value: JsonValue): AgentHarnessUserInputQuestion | undefined {
|
||||
function readQuestion(value: JsonValue): UserInputQuestion | undefined {
|
||||
if (!isJsonObject(value)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -172,17 +181,17 @@ function readQuestion(value: JsonValue): AgentHarnessUserInputQuestion | undefin
|
||||
};
|
||||
}
|
||||
|
||||
function readOptions(value: JsonValue | undefined): AgentHarnessUserInputOption[] | null {
|
||||
function readOptions(value: JsonValue | undefined): UserInputOption[] | null {
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const options = value
|
||||
.map(readOption)
|
||||
.filter((option): option is AgentHarnessUserInputOption => Boolean(option));
|
||||
.filter((option): option is UserInputOption => Boolean(option));
|
||||
return options.length > 0 ? options : null;
|
||||
}
|
||||
|
||||
function readOption(value: JsonValue): AgentHarnessUserInputOption | undefined {
|
||||
function readOption(value: JsonValue): UserInputOption | undefined {
|
||||
if (!isJsonObject(value)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -193,25 +202,116 @@ function readOption(value: JsonValue): AgentHarnessUserInputOption | undefined {
|
||||
|
||||
async function deliverUserInputPrompt(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
questions: AgentHarnessUserInputQuestion[],
|
||||
questions: UserInputQuestion[],
|
||||
): Promise<void> {
|
||||
await deliverAgentHarnessUserInputPrompt(params, questions, {
|
||||
formatText: formatCodexDisplayText,
|
||||
intro: "Codex needs input:",
|
||||
});
|
||||
const text = formatUserInputPrompt(questions);
|
||||
if (params.onBlockReply) {
|
||||
await params.onBlockReply({ text });
|
||||
return;
|
||||
}
|
||||
await params.onPartialReply?.({ text });
|
||||
}
|
||||
|
||||
function buildUserInputResponse(
|
||||
questions: AgentHarnessUserInputQuestion[],
|
||||
inputText: string,
|
||||
): JsonObject {
|
||||
function formatUserInputPrompt(questions: UserInputQuestion[]): string {
|
||||
const lines = ["Codex needs input:"];
|
||||
questions.forEach((question, index) => {
|
||||
if (questions.length > 1) {
|
||||
lines.push(
|
||||
"",
|
||||
`${index + 1}. ${formatCodexDisplayText(question.header)}`,
|
||||
formatCodexDisplayText(question.question),
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"",
|
||||
formatCodexDisplayText(question.header),
|
||||
formatCodexDisplayText(question.question),
|
||||
);
|
||||
}
|
||||
if (question.isSecret) {
|
||||
lines.push("This channel may show your reply to other participants.");
|
||||
}
|
||||
question.options?.forEach((option, optionIndex) => {
|
||||
lines.push(
|
||||
`${optionIndex + 1}. ${formatCodexDisplayText(option.label)}${
|
||||
option.description ? ` - ${formatCodexDisplayText(option.description)}` : ""
|
||||
}`,
|
||||
);
|
||||
});
|
||||
if (question.isOther) {
|
||||
lines.push("Other: reply with your own answer.");
|
||||
}
|
||||
});
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildUserInputResponse(questions: UserInputQuestion[], inputText: string): JsonObject {
|
||||
// Multi-question replies may use "header: answer" or numbered lines. Keep the
|
||||
// parser permissive so chat-channel replies remain ergonomic.
|
||||
return buildAgentHarnessUserInputAnswers(questions, inputText) as unknown as JsonObject;
|
||||
const answers: JsonObject = {};
|
||||
if (questions.length === 1) {
|
||||
const question = questions[0];
|
||||
if (question) {
|
||||
const answer = normalizeAnswer(inputText, question);
|
||||
answers[question.id] = { answers: answer ? [answer] : [] };
|
||||
}
|
||||
return { answers };
|
||||
}
|
||||
|
||||
const keyed = parseKeyedAnswers(inputText);
|
||||
const fallbackLines = inputText
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
questions.forEach((question, index) => {
|
||||
const key =
|
||||
keyed.get(question.id.toLowerCase()) ??
|
||||
keyed.get(question.header.toLowerCase()) ??
|
||||
keyed.get(question.question.toLowerCase()) ??
|
||||
keyed.get(String(index + 1));
|
||||
const answer = key ?? fallbackLines[index] ?? "";
|
||||
const normalized = answer ? normalizeAnswer(answer, question) : undefined;
|
||||
answers[question.id] = { answers: normalized ? [normalized] : [] };
|
||||
});
|
||||
return { answers };
|
||||
}
|
||||
|
||||
function normalizeAnswer(answer: string, question: UserInputQuestion): string | undefined {
|
||||
const trimmed = answer.trim();
|
||||
const options = question.options ?? [];
|
||||
const optionIndex = /^\d+$/.test(trimmed) ? Number(trimmed) - 1 : -1;
|
||||
const indexed = optionIndex >= 0 ? options[optionIndex] : undefined;
|
||||
if (indexed) {
|
||||
return indexed.label;
|
||||
}
|
||||
const exact = options.find((option) => option.label.toLowerCase() === trimmed.toLowerCase());
|
||||
if (exact) {
|
||||
return exact.label;
|
||||
}
|
||||
if (options.length > 0 && !question.isOther) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function parseKeyedAnswers(inputText: string): Map<string, string> {
|
||||
const answers = new Map<string, string>();
|
||||
for (const line of inputText.split(/\r?\n/)) {
|
||||
const match = line.match(/^\s*([^:=-]+?)\s*[:=-]\s*(.+?)\s*$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const key = match[1]?.trim().toLowerCase();
|
||||
const value = match[2]?.trim();
|
||||
if (key && value) {
|
||||
answers.set(key, value);
|
||||
}
|
||||
}
|
||||
return answers;
|
||||
}
|
||||
|
||||
function emptyUserInputResponse(): JsonObject {
|
||||
return emptyAgentHarnessUserInputAnswers() as unknown as JsonObject;
|
||||
return { answers: {} };
|
||||
}
|
||||
|
||||
function readString(record: JsonObject, key: string): string | undefined {
|
||||
|
||||
@@ -1609,12 +1609,6 @@ describe("createCopilotAgentHarness", () => {
|
||||
success: true,
|
||||
tokensRemoved: 123,
|
||||
messagesRemoved: 4,
|
||||
summaryContent: "compacted summary",
|
||||
contextWindow: {
|
||||
tokenLimit: 1000,
|
||||
currentTokens: 777,
|
||||
messagesLength: 12,
|
||||
},
|
||||
}));
|
||||
const disconnect = vi.fn(async () => {
|
||||
throw new Error("disconnect failed");
|
||||
@@ -1655,7 +1649,6 @@ describe("createCopilotAgentHarness", () => {
|
||||
model: "gpt-4.1",
|
||||
sessionKey: "agent:main:main",
|
||||
sessionId: "oc-sess-compact-1",
|
||||
currentTokenCount: 900,
|
||||
workspaceDir: "/this\u0000is/illegal",
|
||||
customInstructions: "Keep decisions.",
|
||||
});
|
||||
@@ -1691,25 +1684,6 @@ describe("createCopilotAgentHarness", () => {
|
||||
ok: true,
|
||||
compacted: true,
|
||||
reason: "copilot-sdk-history-compacted",
|
||||
result: {
|
||||
summary: "compacted summary",
|
||||
firstKeptEntryId: "",
|
||||
tokensBefore: 900,
|
||||
tokensAfter: 777,
|
||||
details: {
|
||||
success: true,
|
||||
tokensRemoved: 123,
|
||||
messagesRemoved: 4,
|
||||
summaryContent: "compacted summary",
|
||||
contextWindow: {
|
||||
tokenLimit: 1000,
|
||||
currentTokens: 777,
|
||||
messagesLength: 12,
|
||||
},
|
||||
},
|
||||
sessionId: "oc-sess-compact-1",
|
||||
sessionFile: "/session.json",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -62,14 +62,6 @@ interface CopilotHistoryCompactResult {
|
||||
tokensRemoved: number;
|
||||
messagesRemoved: number;
|
||||
summaryContent?: string;
|
||||
contextWindow?: {
|
||||
tokenLimit: number;
|
||||
currentTokens: number;
|
||||
messagesLength: number;
|
||||
systemTokens?: number;
|
||||
conversationTokens?: number;
|
||||
toolDefinitionsTokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CopilotHistoryCompactSession {
|
||||
@@ -880,21 +872,6 @@ export function createCopilotAgentHarness(
|
||||
ok: true,
|
||||
compacted,
|
||||
reason: compacted ? "copilot-sdk-history-compacted" : "already under target",
|
||||
...(compacted
|
||||
? {
|
||||
result: {
|
||||
summary: compactResult.summaryContent ?? "",
|
||||
firstKeptEntryId: "",
|
||||
tokensBefore:
|
||||
params.currentTokenCount ??
|
||||
(compactResult.contextWindow?.currentTokens ?? 0) + compactResult.tokensRemoved,
|
||||
tokensAfter: compactResult.contextWindow?.currentTokens,
|
||||
details: compactResult,
|
||||
sessionId: params.sessionId,
|
||||
sessionFile: params.sessionFile,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@ import fsp from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import type { CopilotClient, Tool as SdkTool } from "@github/copilot-sdk";
|
||||
import {
|
||||
abortAgentHarnessRun,
|
||||
queueAgentHarnessMessage,
|
||||
type AgentHarnessAttemptParams,
|
||||
type AgentHarnessAttemptResult,
|
||||
import type {
|
||||
AgentHarnessAttemptParams,
|
||||
AgentHarnessAttemptResult,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import type { SandboxContext } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
@@ -1173,36 +1171,6 @@ describe("runCopilotAttempt", () => {
|
||||
expect(result.externalAbort).toBe(true);
|
||||
});
|
||||
|
||||
it("active-run abort path marks the attempt as externally aborted", async () => {
|
||||
const sendDeferred = createDeferred<SessionEventShape | undefined>();
|
||||
const sessionCreated = createDeferred<FakeSession>();
|
||||
const sdk = makeFakeSdk({
|
||||
onCreateSession: (session) => {
|
||||
session.sendAndWait.mockReturnValue(sendDeferred.promise);
|
||||
session.abort.mockImplementationOnce(async () => {
|
||||
sendDeferred.resolve(undefined);
|
||||
});
|
||||
sessionCreated.resolve(session);
|
||||
},
|
||||
});
|
||||
const pool = makeFakePool(sdk);
|
||||
const createToolBridge = vi.fn(async () => ({ sdkTools: [], sourceTools: [] }));
|
||||
|
||||
const runPromise = runCopilotAttempt(makeParams(), {
|
||||
createToolBridge,
|
||||
pool,
|
||||
});
|
||||
const session = await sessionCreated.promise;
|
||||
await vi.waitFor(() => expect(session.sendAndWait).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(abortAgentHarnessRun("session-1")).toBe(true);
|
||||
const result = await runPromise;
|
||||
|
||||
expect(session.abort).toHaveBeenCalledTimes(1);
|
||||
expect(result.aborted).toBe(true);
|
||||
expect(result.externalAbort).toBe(true);
|
||||
});
|
||||
|
||||
it("abort path (signal already aborted)", async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
@@ -1479,42 +1447,18 @@ describe("runCopilotAttempt", () => {
|
||||
expect(result.feedback).toContain("no permission policy installed");
|
||||
});
|
||||
|
||||
it("registers ask_user and resolves it from the active OpenClaw queue", async () => {
|
||||
const onBlockReply = vi.fn();
|
||||
const sdk = makeFakeSdk({
|
||||
onCreateSession: (session, cfg) => {
|
||||
session.sendAndWait.mockImplementationOnce(async () => {
|
||||
const handler = cfg.onUserInputRequest;
|
||||
if (typeof handler !== "function") {
|
||||
throw new Error("expected onUserInputRequest handler");
|
||||
}
|
||||
const response = await handler(
|
||||
{
|
||||
question: "Pick a mode",
|
||||
choices: ["Fast", "Deep"],
|
||||
allowFreeform: false,
|
||||
},
|
||||
{ sessionId: session.sessionId },
|
||||
);
|
||||
return makeAssistantMessageEvent(`selected ${response.answer}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
it("does not register onUserInputRequest (ask_user hidden from the model in MVP)", async () => {
|
||||
const sdk = makeFakeSdk();
|
||||
const pool = makeFakePool(sdk);
|
||||
|
||||
const attempt = runCopilotAttempt(makeParams({ onBlockReply }), { pool });
|
||||
|
||||
await vi.waitFor(() => expect(onBlockReply).toHaveBeenCalledTimes(1));
|
||||
expect(queueAgentHarnessMessage("session-1", "2")).toBe(true);
|
||||
const result = await attempt;
|
||||
await runCopilotAttempt(makeParams(), { pool });
|
||||
|
||||
const cfg = sdk.createSession.mock.calls[0]?.[0];
|
||||
expect(typeof cfg.onUserInputRequest).toBe("function");
|
||||
expect(onBlockReply.mock.calls[0]?.[0]).toEqual(
|
||||
expect.objectContaining({ text: expect.stringContaining("Pick a mode") }),
|
||||
);
|
||||
expect(result.assistantTexts).toEqual(["selected Deep"]);
|
||||
expect(queueAgentHarnessMessage("session-1", "late")).toBe(false);
|
||||
// Per the SDK contract (types.d.ts: `When provided, enables the
|
||||
// ask_user tool allowing the agent to ask questions`), omitting the
|
||||
// handler hides ask_user from the model entirely. The MVP keeps it
|
||||
// hidden until a real channel/TUI prompt bridge exists.
|
||||
expect("onUserInputRequest" in cfg).toBe(false);
|
||||
});
|
||||
|
||||
it("enableSessionTelemetry is omitted from createSession when undefined (SDK default)", async () => {
|
||||
@@ -1910,7 +1854,6 @@ describe("runCopilotAttempt", () => {
|
||||
it("retains a timed-out session until later compaction reaches session.idle", async () => {
|
||||
const afterCompaction = vi.fn();
|
||||
const onDeferredCompaction = vi.fn();
|
||||
const cleanupToolBridge = vi.fn();
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "after_compaction", handler: afterCompaction }]),
|
||||
);
|
||||
@@ -1923,14 +1866,8 @@ describe("runCopilotAttempt", () => {
|
||||
);
|
||||
},
|
||||
});
|
||||
const createToolBridge = vi.fn(async () => ({
|
||||
cleanup: cleanupToolBridge,
|
||||
sdkTools: [],
|
||||
sourceTools: [],
|
||||
}));
|
||||
|
||||
const result = await runCopilotAttempt(makeParams(), {
|
||||
createToolBridge,
|
||||
onDeferredCompaction,
|
||||
pool: makeFakePool(sdk),
|
||||
});
|
||||
@@ -1940,7 +1877,6 @@ describe("runCopilotAttempt", () => {
|
||||
expect(onDeferredCompaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sdkSessionId: "sess-1" }),
|
||||
);
|
||||
expect(cleanupToolBridge).not.toHaveBeenCalled();
|
||||
expect(activeSession?.disconnect).not.toHaveBeenCalled();
|
||||
|
||||
activeSession?.emit("session.compaction_start", {});
|
||||
@@ -1954,7 +1890,6 @@ describe("runCopilotAttempt", () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(activeSession?.disconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(cleanupToolBridge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not mark a timeout after SDK compaction has completed as active compaction", async () => {
|
||||
@@ -3131,8 +3066,7 @@ describe("runCopilotAttempt", () => {
|
||||
// permission policy and pollute the catalog under the default reject
|
||||
// policy. `createSessionConfig` derives `availableTools` from the
|
||||
// post-filter `sdkTools` so create- and resume-session always carry
|
||||
// exactly the names of the tools the bridge actually exposed plus the
|
||||
// built-in `ask_user` tool owned by the registered user-input handler.
|
||||
// exactly the names of the tools the bridge actually exposed.
|
||||
describe("availableTools surface restriction (PR #86155 [P1] round-8)", () => {
|
||||
function makeFakeSdkTool(name: string): SdkTool {
|
||||
return {
|
||||
@@ -3156,11 +3090,7 @@ describe("runCopilotAttempt", () => {
|
||||
|
||||
await runCopilotAttempt(makeParams(), { createToolBridge, pool });
|
||||
|
||||
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual([
|
||||
"read",
|
||||
"edit",
|
||||
"builtin:ask_user",
|
||||
]);
|
||||
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual(["read", "edit"]);
|
||||
});
|
||||
|
||||
it("forwards `[]` to the SDK when the bridge returns no tools (disable / raw / fully filtered)", async () => {
|
||||
@@ -3170,13 +3100,12 @@ describe("runCopilotAttempt", () => {
|
||||
// (`modelRun: true` or `promptMode: "none"`), an empty
|
||||
// `toolsAllow: []`, and an unsupported provider to `sdkTools: []`.
|
||||
// Whatever the upstream reason, `availableTools` must be the same
|
||||
// ask_user-only list so the SDK cannot fall back to its native
|
||||
// catalog while the registered user-input handler remains usable.
|
||||
// empty list so the SDK cannot fall back to its native catalog.
|
||||
const createToolBridge = vi.fn(async () => ({ sdkTools: [], sourceTools: [] }));
|
||||
|
||||
await runCopilotAttempt(makeParams(), { createToolBridge, pool });
|
||||
|
||||
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual(["builtin:ask_user"]);
|
||||
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual([]);
|
||||
});
|
||||
|
||||
it("forwards the full bridged set when the run is unrestricted (no toolsAllow)", async () => {
|
||||
@@ -3202,7 +3131,6 @@ describe("runCopilotAttempt", () => {
|
||||
"edit",
|
||||
"exec",
|
||||
"message",
|
||||
"builtin:ask_user",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -3228,7 +3156,7 @@ describe("runCopilotAttempt", () => {
|
||||
|
||||
const resumeCall = sdk.resumeSession.mock.calls[0] as unknown[] | undefined;
|
||||
const resumeCfg = resumeCall?.[1] as { availableTools?: string[] };
|
||||
expect(resumeCfg?.availableTools).toEqual(["read", "builtin:ask_user"]);
|
||||
expect(resumeCfg?.availableTools).toEqual(["read"]);
|
||||
});
|
||||
|
||||
it("forwards `[]` to resumeSession when the bridge returns no tools", async () => {
|
||||
@@ -3247,7 +3175,7 @@ describe("runCopilotAttempt", () => {
|
||||
|
||||
const resumeCall = sdk.resumeSession.mock.calls[0] as unknown[] | undefined;
|
||||
const resumeCfg = resumeCall?.[1] as { availableTools?: string[] };
|
||||
expect(resumeCfg?.availableTools).toEqual(["builtin:ask_user"]);
|
||||
expect(resumeCfg?.availableTools).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
runAgentEndSideEffects,
|
||||
runAgentHarnessLlmInputHook,
|
||||
runAgentHarnessLlmOutputHook,
|
||||
clearActiveEmbeddedRun,
|
||||
setActiveEmbeddedRun,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { resolveCopilotAuth } from "./auth-bridge.js";
|
||||
import {
|
||||
@@ -57,12 +55,10 @@ import {
|
||||
} from "./replay-shim.js";
|
||||
import type { ClientCreateOptions, CopilotClientPool, PoolKey, PooledClient } from "./runtime.js";
|
||||
import { createCopilotToolBridge } from "./tool-bridge.js";
|
||||
import { createCopilotUserInputBridge } from "./user-input-bridge.js";
|
||||
import { resolveCopilotWorkspaceBootstrapContext } from "./workspace-bootstrap.js";
|
||||
|
||||
const SUPPORTED_PROVIDERS = new Set(["github-copilot"]);
|
||||
const BACKGROUND_COMPACTION_CANCEL_TIMEOUT_MS = 5_000;
|
||||
const COPILOT_ASK_USER_AVAILABLE_TOOLS = ["builtin:ask_user"] as const;
|
||||
|
||||
type AttemptResultWithSdkSessionId = AgentHarnessAttemptResult & { sdkSessionId?: string };
|
||||
type PromptErrorWithCode = Error & { code?: string; cause?: unknown };
|
||||
@@ -77,7 +73,6 @@ export type CopilotSessionConfig = Pick<
|
||||
| "infiniteSessions"
|
||||
| "model"
|
||||
| "onPermissionRequest"
|
||||
| "onUserInputRequest"
|
||||
| "reasoningEffort"
|
||||
| "systemMessage"
|
||||
| "tools"
|
||||
@@ -227,7 +222,6 @@ function deferBackgroundCompactionCleanup(params: {
|
||||
bridge: ReturnType<typeof attachEventBridge>;
|
||||
handle: PooledClient;
|
||||
pool: CopilotClientPool;
|
||||
cleanupToolBridge?: () => void;
|
||||
sdkSessionId?: string;
|
||||
session: SessionLike;
|
||||
timeoutMs: number;
|
||||
@@ -256,7 +250,6 @@ function deferBackgroundCompactionCleanup(params: {
|
||||
} catch {
|
||||
// The attempt has already returned its timeout result.
|
||||
}
|
||||
params.cleanupToolBridge?.();
|
||||
if (outcome !== "completed" && params.sdkSessionId) {
|
||||
try {
|
||||
await params.handle.client.deleteSession(params.sdkSessionId);
|
||||
@@ -410,9 +403,6 @@ export async function runCopilotAttempt(
|
||||
let handle: PooledClient | undefined;
|
||||
let session: SessionLike | undefined;
|
||||
let bridge: ReturnType<typeof attachEventBridge> | undefined;
|
||||
let activeRunHandleRef: Parameters<typeof clearActiveEmbeddedRun>[1] | undefined;
|
||||
let userInputBridgeRef: ReturnType<typeof createCopilotUserInputBridge> | undefined;
|
||||
let cleanupToolBridge: (() => void) | undefined;
|
||||
let releaseError: Error | undefined;
|
||||
let downgradedFromResume = false;
|
||||
let resumeFailureRecovered = false;
|
||||
@@ -425,24 +415,16 @@ export async function runCopilotAttempt(
|
||||
// `src/agents/pi-embedded-runner/run/types.ts:139`.
|
||||
let yieldDetected = false;
|
||||
|
||||
const markExternalAbort = () => {
|
||||
const onAbort = () => {
|
||||
abortRequested = true;
|
||||
externalAbort = true;
|
||||
aborted = true;
|
||||
};
|
||||
|
||||
const abortActiveSession = () => {
|
||||
markExternalAbort();
|
||||
if (settled || !sentTurnStarted || !session) {
|
||||
return;
|
||||
}
|
||||
void session.abort().catch(() => undefined);
|
||||
};
|
||||
|
||||
const onAbort = () => {
|
||||
abortActiveSession();
|
||||
};
|
||||
|
||||
params.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
||||
|
||||
// Sandbox parity with PI (`src/agents/pi-embedded-runner/run/attempt.ts:1232-1244`):
|
||||
@@ -593,7 +575,6 @@ export async function runCopilotAttempt(
|
||||
startedAt,
|
||||
}),
|
||||
});
|
||||
cleanupToolBridge = toolBridge.cleanup;
|
||||
sdkTools = toolBridge.sdkTools;
|
||||
} catch (error: unknown) {
|
||||
const result = createResult(input, {
|
||||
@@ -674,11 +655,6 @@ export async function runCopilotAttempt(
|
||||
});
|
||||
};
|
||||
const hasNativePromptHook = Boolean(attemptInput.hooksConfig?.onUserPromptSubmitted);
|
||||
const userInputBridge = createCopilotUserInputBridge({
|
||||
paramsForRun: attemptInput,
|
||||
signal: params.abortSignal,
|
||||
});
|
||||
userInputBridgeRef = userInputBridge;
|
||||
const sessionConfig = createSessionConfig(
|
||||
attemptInput,
|
||||
modelRef.id,
|
||||
@@ -687,7 +663,6 @@ export async function runCopilotAttempt(
|
||||
promptBuild.developerInstructions || undefined,
|
||||
effectiveWorkspaceDir,
|
||||
effectiveCwd,
|
||||
userInputBridge.onUserInputRequest,
|
||||
hasNativePromptHook
|
||||
? {
|
||||
onUserPromptSubmitted: ({ additionalContext, prompt }) =>
|
||||
@@ -773,29 +748,6 @@ export async function runCopilotAttempt(
|
||||
isAborted: () => aborted,
|
||||
});
|
||||
|
||||
const activeRunHandle = {
|
||||
kind: "embedded" as const,
|
||||
queueMessage: async (text: string) => {
|
||||
if (userInputBridge.handleQueuedMessage(text)) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Copilot runtime is not waiting for user input.");
|
||||
},
|
||||
isStreaming: () => !settled && !aborted,
|
||||
isCompacting: () => bridge?.isCompacting() ?? false,
|
||||
sourceReplyDeliveryMode: input.sourceReplyDeliveryMode,
|
||||
cancel: () => {
|
||||
userInputBridge.cancelPending();
|
||||
abortActiveSession();
|
||||
},
|
||||
abort: () => {
|
||||
userInputBridge.cancelPending();
|
||||
abortActiveSession();
|
||||
},
|
||||
};
|
||||
setActiveEmbeddedRun(input.sessionId, activeRunHandle, input.sessionKey, input.sessionFile);
|
||||
activeRunHandleRef = activeRunHandle;
|
||||
|
||||
const messageOptions = await createMessageOptions(attemptInput, {
|
||||
effectiveCwd,
|
||||
effectiveWorkspaceDir,
|
||||
@@ -854,15 +806,6 @@ export async function runCopilotAttempt(
|
||||
}
|
||||
} finally {
|
||||
settled = true;
|
||||
userInputBridgeRef?.cancelPending();
|
||||
if (activeRunHandleRef) {
|
||||
clearActiveEmbeddedRun(
|
||||
input.sessionId,
|
||||
activeRunHandleRef,
|
||||
input.sessionKey,
|
||||
input.sessionFile,
|
||||
);
|
||||
}
|
||||
const retainSessionForDeferredCleanup =
|
||||
bridge?.hasObservedCompaction() || (timedOut && bridge?.hasObservedSessionIdle() === false);
|
||||
if (retainSessionForDeferredCleanup && bridge && session && handle) {
|
||||
@@ -877,7 +820,6 @@ export async function runCopilotAttempt(
|
||||
abortSignal: cleanupAbort.signal,
|
||||
awaitSessionIdle: !bridge.hasObservedSessionIdle(),
|
||||
bridge,
|
||||
cleanupToolBridge,
|
||||
handle,
|
||||
pool: deps.pool,
|
||||
sdkSessionId,
|
||||
@@ -906,7 +848,6 @@ export async function runCopilotAttempt(
|
||||
// defines as no background agents in flight. Timeouts retain the bridge
|
||||
// until that event so compaction that starts after the timer still completes.
|
||||
await bridge?.awaitCompactionChain();
|
||||
cleanupToolBridge?.();
|
||||
bridge?.detach();
|
||||
params.abortSignal?.removeEventListener("abort", onAbort);
|
||||
|
||||
@@ -1177,7 +1118,6 @@ function createSessionConfig(
|
||||
systemMessageContent: string | undefined,
|
||||
effectiveWorkspaceDir: string | undefined,
|
||||
effectiveCwd: string | undefined,
|
||||
onUserInputRequest: NonNullable<SessionConfig["onUserInputRequest"]>,
|
||||
hooksBridgeOptions?: Parameters<typeof createHooksBridge>[1],
|
||||
): CopilotSessionConfig {
|
||||
const permissionPolicy = params.permissionPolicy ?? rejectAllPolicy;
|
||||
@@ -1205,9 +1145,10 @@ function createSessionConfig(
|
||||
// tool wrapper, and the SDK gate is a safety net for kinds we
|
||||
// don't surface. See permission-bridge.ts and docs/plugins/copilot.md.
|
||||
onPermissionRequest: createPermissionBridge(permissionPolicy),
|
||||
// Registers the SDK ask_user bridge. The bridge itself owns pending
|
||||
// reply routing so generic mid-run steering still fails closed.
|
||||
onUserInputRequest,
|
||||
// `onUserInputRequest` is intentionally NOT registered: per the SDK
|
||||
// contract, omitting the handler hides the `ask_user` tool from the
|
||||
// model entirely. Interactive ask_user will need a real channel/TUI
|
||||
// prompt bridge before this runtime can expose the handler.
|
||||
// Preserve the shipped native SDK hook contract. These callbacks expose
|
||||
// Copilot-specific events and decisions that generic lifecycle hooks do
|
||||
// not model.
|
||||
@@ -1225,9 +1166,8 @@ function createSessionConfig(
|
||||
...(infiniteSessions ? { infiniteSessions } : {}),
|
||||
reasoningEffort: params.reasoningEffort,
|
||||
tools: sdkTools,
|
||||
// Restrict the SDK's tool catalog to the bridged tool names returned
|
||||
// by `createCopilotToolBridge` plus the built-in `ask_user` tool owned
|
||||
// by `onUserInputRequest`. Without this, the SDK
|
||||
// Restrict the SDK's tool catalog to exactly the bridged tool names
|
||||
// returned by `createCopilotToolBridge`. Without this, the SDK
|
||||
// would still expose its native read/write/shell/url/mcp/memory/
|
||||
// hook tools to the model alongside our overrides, which would
|
||||
// bypass OpenClaw's wrapped-tool enforcement under any permissive
|
||||
@@ -1242,7 +1182,7 @@ function createSessionConfig(
|
||||
// `@github/copilot-sdk/dist/types.d.ts:1198` (it picks
|
||||
// `availableTools`, so the spread into `resumeSession` covers
|
||||
// the resume path too).
|
||||
availableTools: buildCopilotAvailableTools(sdkTools),
|
||||
availableTools: sdkTools.map((tool) => tool.name),
|
||||
workingDirectory:
|
||||
effectiveCwd ?? effectiveWorkspaceDir ?? readResolvedAttemptPath(params.workspaceDir),
|
||||
// When a task runs from a sub-cwd, keep SDK-native project docs
|
||||
@@ -1288,10 +1228,6 @@ function createSessionConfig(
|
||||
};
|
||||
}
|
||||
|
||||
function buildCopilotAvailableTools(sdkTools: SdkTool[]): string[] {
|
||||
return [...new Set([...sdkTools.map((tool) => tool.name), ...COPILOT_ASK_USER_AVAILABLE_TOOLS])];
|
||||
}
|
||||
|
||||
async function createMessageOptions(
|
||||
params: AttemptParamsLike,
|
||||
context: {
|
||||
|
||||
@@ -156,184 +156,11 @@ describe("createCopilotToolBridge", () => {
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(result.sourceTools).toEqual(sourceTools);
|
||||
expect(result.sourceTools).toBe(sourceTools);
|
||||
expect(result.sdkTools).toHaveLength(2);
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["tool-a", "tool-b"]);
|
||||
});
|
||||
|
||||
it("compacts the Copilot tool surface behind tool_search controls when enabled", async () => {
|
||||
const createOpenClawCodingTools = vi.fn(async (opts: unknown) => {
|
||||
const includeToolSearchControls = Boolean(
|
||||
(opts as { includeToolSearchControls?: boolean }).includeToolSearchControls,
|
||||
);
|
||||
return includeToolSearchControls
|
||||
? [
|
||||
makeTool({ name: "tool_search_code" }),
|
||||
makeTool({ name: "fake_hidden" }),
|
||||
makeTool({ name: "read" }),
|
||||
]
|
||||
: [makeTool({ name: "fake_hidden" }), makeTool({ name: "read" })];
|
||||
});
|
||||
|
||||
const result = await createCopilotToolBridge({
|
||||
agentId: "agent-1",
|
||||
attemptParams: {
|
||||
config: { tools: { toolSearch: true } },
|
||||
runId: "run-tool-search",
|
||||
sessionKey: "agent:main:main",
|
||||
} as never,
|
||||
createOpenClawCodingTools,
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(createOpenClawCodingTools).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
includeToolSearchControls: true,
|
||||
toolSearchCatalogRef: expect.any(Object),
|
||||
toolSearchCatalogExecutor: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(result.sourceTools.map((tool) => tool.name)).toEqual(["tool_search_code"]);
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["tool_search_code"]);
|
||||
});
|
||||
|
||||
it("keeps tool_search controls visible when a narrow allowlist is active", async () => {
|
||||
const createOpenClawCodingTools = vi.fn(async (opts: unknown) => {
|
||||
const includeToolSearchControls = Boolean(
|
||||
(opts as { includeToolSearchControls?: boolean }).includeToolSearchControls,
|
||||
);
|
||||
return includeToolSearchControls
|
||||
? [makeTool({ name: "tool_search_code" }), makeTool({ name: "read" })]
|
||||
: [makeTool({ name: "read" })];
|
||||
});
|
||||
|
||||
const result = await createCopilotToolBridge({
|
||||
agentId: "agent-1",
|
||||
attemptParams: {
|
||||
config: { tools: { toolSearch: true } },
|
||||
runId: "run-tool-search",
|
||||
sessionKey: "agent:main:main",
|
||||
toolsAllow: ["read"],
|
||||
} as never,
|
||||
createOpenClawCodingTools,
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(result.sourceTools.map((tool) => tool.name)).toEqual(["tool_search_code"]);
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["tool_search_code"]);
|
||||
});
|
||||
|
||||
it("filters the hidden tool_search catalog before compacting narrowed tools", async () => {
|
||||
let catalogRef: { current?: { entries?: Array<{ name: string }> } } | undefined;
|
||||
const createOpenClawCodingTools = vi.fn(async (opts: unknown) => {
|
||||
catalogRef = (opts as { toolSearchCatalogRef?: typeof catalogRef }).toolSearchCatalogRef;
|
||||
return [
|
||||
makeTool({ name: "tool_search_code" }),
|
||||
makeTool({ name: "read" }),
|
||||
makeTool({ name: "edit" }),
|
||||
makeTool({ name: "write" }),
|
||||
];
|
||||
});
|
||||
|
||||
await createCopilotToolBridge({
|
||||
agentId: "agent-1",
|
||||
attemptParams: {
|
||||
config: { tools: { toolSearch: true } },
|
||||
runId: "run-tool-search",
|
||||
sessionKey: "agent:main:main",
|
||||
toolsAllow: ["read"],
|
||||
} as never,
|
||||
createOpenClawCodingTools,
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(catalogRef?.current?.entries?.map((entry) => entry.name)).toEqual(["read"]);
|
||||
});
|
||||
|
||||
it("compacts the Copilot tool surface behind code-mode exec/wait when enabled", async () => {
|
||||
const createOpenClawCodingTools = vi.fn(async () => [
|
||||
makeTool({ name: "fake_hidden" }),
|
||||
makeTool({ name: "read" }),
|
||||
]);
|
||||
|
||||
const result = await createCopilotToolBridge({
|
||||
agentId: "agent-1",
|
||||
attemptParams: {
|
||||
config: { tools: { codeMode: true } },
|
||||
runId: "run-code-mode",
|
||||
sessionKey: "agent:main:main",
|
||||
} as never,
|
||||
createOpenClawCodingTools,
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(createOpenClawCodingTools).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
includeToolSearchControls: false,
|
||||
toolSearchCatalogRef: expect.any(Object),
|
||||
toolSearchCatalogExecutor: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(result.sourceTools.map((tool) => tool.name)).toEqual(["exec", "wait"]);
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["exec", "wait"]);
|
||||
});
|
||||
|
||||
it("keeps code-mode controls visible when a narrow allowlist is active", async () => {
|
||||
const createOpenClawCodingTools = vi.fn(async () => [
|
||||
makeTool({ name: "fake_hidden" }),
|
||||
makeTool({ name: "read" }),
|
||||
]);
|
||||
|
||||
const result = await createCopilotToolBridge({
|
||||
agentId: "agent-1",
|
||||
attemptParams: {
|
||||
config: { tools: { codeMode: true } },
|
||||
runId: "run-code-mode",
|
||||
sessionKey: "agent:main:main",
|
||||
toolsAllow: ["read"],
|
||||
} as never,
|
||||
createOpenClawCodingTools,
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(result.sourceTools.map((tool) => tool.name)).toEqual(["exec", "wait"]);
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["exec", "wait"]);
|
||||
});
|
||||
|
||||
it("filters the hidden code-mode catalog before compacting narrowed tools", async () => {
|
||||
let catalogRef: { current?: { entries?: Array<{ name: string }> } } | undefined;
|
||||
const createOpenClawCodingTools = vi.fn(async (opts: unknown) => {
|
||||
catalogRef = (opts as { toolSearchCatalogRef?: typeof catalogRef }).toolSearchCatalogRef;
|
||||
return [makeTool({ name: "read" }), makeTool({ name: "edit" }), makeTool({ name: "write" })];
|
||||
});
|
||||
|
||||
await createCopilotToolBridge({
|
||||
agentId: "agent-1",
|
||||
attemptParams: {
|
||||
config: { tools: { codeMode: true } },
|
||||
runId: "run-code-mode",
|
||||
sessionKey: "agent:main:main",
|
||||
toolsAllow: ["read"],
|
||||
} as never,
|
||||
createOpenClawCodingTools,
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(catalogRef?.current?.entries?.map((entry) => entry.name)).toEqual(["read"]);
|
||||
});
|
||||
|
||||
it("throws when createOpenClawCodingTools returns a non-array", async () => {
|
||||
await expect(
|
||||
createCopilotToolBridge({
|
||||
|
||||
@@ -17,15 +17,10 @@ import {
|
||||
resolveModelAuthMode,
|
||||
sanitizeToolResult,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { createAgentHarnessToolSurfaceRuntime } from "openclaw/plugin-sdk/agent-harness-tool-runtime";
|
||||
|
||||
type CreateOpenClawCodingTools =
|
||||
(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"];
|
||||
type OpenClawCodingToolsOptions = NonNullable<Parameters<CreateOpenClawCodingTools>[0]>;
|
||||
type AgentHarnessToolSurfaceRuntime = ReturnType<typeof createAgentHarnessToolSurfaceRuntime>;
|
||||
type CatalogExecuteParams = Parameters<
|
||||
NonNullable<AgentHarnessToolSurfaceRuntime["toolSearchCatalogExecutor"]>
|
||||
>[0];
|
||||
|
||||
type AgentToolResultLike = {
|
||||
content?: unknown;
|
||||
@@ -135,7 +130,6 @@ export interface CopilotToolBridgeInput {
|
||||
}
|
||||
|
||||
export interface CopilotToolBridge {
|
||||
cleanup?: () => void;
|
||||
sdkTools: SdkTool[];
|
||||
sourceTools: AnyAgentTool[];
|
||||
}
|
||||
@@ -184,31 +178,7 @@ export async function createCopilotToolBridge(
|
||||
input.createOpenClawCodingTools ??
|
||||
(await import("openclaw/plugin-sdk/agent-harness")).createOpenClawCodingTools;
|
||||
|
||||
const toolSurfaceRuntime = createAgentHarnessToolSurfaceRuntime({
|
||||
abortSignal: input.abortSignal,
|
||||
agentId: input.agentId,
|
||||
config: attemptParams.config,
|
||||
disableTools: attemptParams.disableTools,
|
||||
executeTool: (toolParams) => executeCatalogTool(input, toolParams),
|
||||
forceMessageTool: shouldForceCopilotMessageTool(attemptParams),
|
||||
isRawModelRun: isCopilotRawModelRun(attemptParams),
|
||||
modelToolsEnabled: true,
|
||||
prompt: attemptParams.prompt,
|
||||
runId: attemptParams.runId,
|
||||
runtimeToolAllowlist: effectiveToolPlan.runtimeToolAllowlist,
|
||||
sessionId: input.sessionId,
|
||||
sessionKey: attemptParams.sandboxSessionKey ?? attemptParams.sessionKey ?? input.sessionKey,
|
||||
sourceReplyDeliveryMode: attemptParams.sourceReplyDeliveryMode,
|
||||
toolsAllow: attemptParams.toolsAllow,
|
||||
});
|
||||
const toolOptions = buildOpenClawCodingToolsOptions(
|
||||
input,
|
||||
{
|
||||
...effectiveToolPlan,
|
||||
runtimeToolAllowlist: toolSurfaceRuntime.runtimeToolAllowlist,
|
||||
},
|
||||
toolSurfaceRuntime,
|
||||
);
|
||||
const toolOptions = buildOpenClawCodingToolsOptions(input, effectiveToolPlan);
|
||||
|
||||
let sourceTools: unknown;
|
||||
try {
|
||||
@@ -226,19 +196,13 @@ export async function createCopilotToolBridge(
|
||||
);
|
||||
}
|
||||
|
||||
const allowedSourceTools = filterCopilotToolsForAllowlist(
|
||||
sourceTools as AnyAgentTool[],
|
||||
toolSurfaceRuntime.runtimeToolAllowlist,
|
||||
);
|
||||
const compactedTools = toolSurfaceRuntime.compactTools(allowedSourceTools);
|
||||
const plannedTools = filterCopilotToolsForConstructionPlan(
|
||||
compactedTools.tools,
|
||||
sourceTools as AnyAgentTool[],
|
||||
effectiveToolPlan.codingToolConstructionPlan,
|
||||
{ preserveToolNames: toolSurfaceRuntime.runtimeToolAllowlist },
|
||||
);
|
||||
const filteredTools = filterCopilotToolsForAllowlist(
|
||||
plannedTools,
|
||||
toolSurfaceRuntime.runtimeToolAllowlist,
|
||||
effectiveToolPlan.runtimeToolAllowlist,
|
||||
);
|
||||
|
||||
// Run duplicate detection after filtering so a duplicate in a
|
||||
@@ -250,7 +214,6 @@ export async function createCopilotToolBridge(
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup: toolSurfaceRuntime.cleanup,
|
||||
sdkTools: filteredTools.map((sourceTool) =>
|
||||
convertOpenClawToolToSdkTool(sourceTool, {
|
||||
abortSignal: input.abortSignal,
|
||||
@@ -288,7 +251,6 @@ export async function createCopilotToolBridge(
|
||||
function buildOpenClawCodingToolsOptions(
|
||||
input: CopilotToolBridgeInput,
|
||||
toolPlan: ReturnType<typeof resolveEmbeddedAttemptToolConstructionPlan>,
|
||||
toolSurfaceRuntime?: ReturnType<typeof createAgentHarnessToolSurfaceRuntime>,
|
||||
): OpenClawCodingToolsOptions {
|
||||
const a = input.attemptParams ?? ({} as CopilotToolAttemptParams);
|
||||
|
||||
@@ -377,14 +339,11 @@ function buildOpenClawCodingToolsOptions(
|
||||
// `resolveSandboxContext`).
|
||||
sandbox,
|
||||
spawnWorkspaceDir,
|
||||
config: toolSurfaceRuntime?.config ?? a.config,
|
||||
config: a.config,
|
||||
abortSignal: input.abortSignal,
|
||||
modelProvider: input.modelProvider,
|
||||
modelId: input.modelId,
|
||||
includeCoreTools: toolPlan.includeCoreTools,
|
||||
includeToolSearchControls: toolSurfaceRuntime?.includeToolSearchControls,
|
||||
toolSearchCatalogRef: toolSurfaceRuntime?.toolSearchCatalogRef,
|
||||
toolSearchCatalogExecutor: toolSurfaceRuntime?.toolSearchCatalogExecutor,
|
||||
runtimeToolAllowlist: toolPlan.runtimeToolAllowlist,
|
||||
toolConstructionPlan: toolPlan.codingToolConstructionPlan,
|
||||
modelCompat,
|
||||
@@ -616,63 +575,6 @@ export function convertOpenClawToolToSdkTool(
|
||||
};
|
||||
}
|
||||
|
||||
async function executeCatalogTool(
|
||||
input: CopilotToolBridgeInput,
|
||||
params: CatalogExecuteParams,
|
||||
): Promise<Awaited<ReturnType<AnyAgentTool["execute"]>>> {
|
||||
const sourceTool = params.tool as AnyAgentTool;
|
||||
const startedAt = Date.now();
|
||||
let preparedArgs: unknown = params.input;
|
||||
try {
|
||||
preparedArgs = sourceTool.prepareArguments
|
||||
? sourceTool.prepareArguments(params.input)
|
||||
: params.input;
|
||||
const result = await sourceTool.execute(
|
||||
params.toolCallId,
|
||||
preparedArgs,
|
||||
params.signal ?? input.abortSignal,
|
||||
params.onUpdate,
|
||||
);
|
||||
const sanitizedResult = sanitizeToolResult(result);
|
||||
const isError = isToolResultError(sanitizedResult);
|
||||
input.attemptParams?.onAgentToolResult?.({
|
||||
toolName: params.toolName,
|
||||
result: sanitizedResult,
|
||||
isError,
|
||||
});
|
||||
await input.onToolCompleted?.({
|
||||
toolName: params.toolName,
|
||||
toolCallId: params.toolCallId,
|
||||
args: toToolStartArgs(preparedArgs),
|
||||
result: sanitizedResult,
|
||||
...(isError
|
||||
? { error: extractToolErrorMessage(sanitizedResult) ?? "tool returned an error" }
|
||||
: {}),
|
||||
startedAt,
|
||||
});
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
const message = toError(error).message;
|
||||
const failure = sanitizeToolResult({
|
||||
content: [{ type: "text", text: message }],
|
||||
details: { status: "failed", error: message },
|
||||
});
|
||||
input.attemptParams?.onAgentToolResult?.({
|
||||
toolName: params.toolName,
|
||||
result: failure,
|
||||
isError: true,
|
||||
});
|
||||
await input.onToolCompleted?.({
|
||||
toolName: params.toolName,
|
||||
toolCallId: params.toolCallId,
|
||||
args: toToolStartArgs(preparedArgs),
|
||||
error: message,
|
||||
startedAt,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function toToolStartArgs(args: unknown): Record<string, unknown> {
|
||||
return args && typeof args === "object" && !Array.isArray(args)
|
||||
? (args as Record<string, unknown>)
|
||||
@@ -810,16 +712,11 @@ function filterCopilotToolsForAllowlist<T extends { name: string }>(
|
||||
function filterCopilotToolsForConstructionPlan<T extends { name: string }>(
|
||||
tools: T[],
|
||||
plan: ReturnType<typeof resolveEmbeddedAttemptToolConstructionPlan>["codingToolConstructionPlan"],
|
||||
options: { preserveToolNames?: readonly string[] } = {},
|
||||
): T[] {
|
||||
if (plan.includeBaseCodingTools && plan.includeShellTools) {
|
||||
return tools;
|
||||
}
|
||||
const preserveToolNames = new Set(options.preserveToolNames);
|
||||
return tools.filter((tool) => {
|
||||
if (preserveToolNames.has(tool.name)) {
|
||||
return true;
|
||||
}
|
||||
if (!plan.includeBaseCodingTools && BASE_COPILOT_CODING_TOOL_NAMES.has(tool.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
// Copilot tests cover SDK ask_user bridge behavior.
|
||||
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createCopilotUserInputBridge } from "./user-input-bridge.js";
|
||||
|
||||
function createParams(): EmbeddedRunAttemptParams {
|
||||
return {
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
onBlockReply: vi.fn(),
|
||||
} as unknown as EmbeddedRunAttemptParams;
|
||||
}
|
||||
|
||||
function expectFirstBlockReplyText(params: EmbeddedRunAttemptParams): string {
|
||||
const onBlockReply = params.onBlockReply;
|
||||
if (!onBlockReply) {
|
||||
throw new Error("Expected onBlockReply callback");
|
||||
}
|
||||
const payload = vi.mocked(onBlockReply).mock.calls[0]?.[0];
|
||||
if (typeof payload?.text !== "string") {
|
||||
throw new Error("Expected first block reply text");
|
||||
}
|
||||
return payload.text;
|
||||
}
|
||||
|
||||
describe("Copilot user input bridge", () => {
|
||||
it("prompts through OpenClaw and resolves the SDK request from the next queued message", async () => {
|
||||
const params = createParams();
|
||||
const bridge = createCopilotUserInputBridge({ paramsForRun: params });
|
||||
|
||||
const response = bridge.onUserInputRequest(
|
||||
{
|
||||
question: "Pick a mode",
|
||||
choices: ["Fast", "Deep"],
|
||||
allowFreeform: false,
|
||||
},
|
||||
{ sessionId: "sdk-session-1" },
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
||||
expect(expectFirstBlockReplyText(params)).toContain("Pick a mode");
|
||||
expect(bridge.handleQueuedMessage("2")).toBe(true);
|
||||
|
||||
await expect(response).resolves.toEqual({ answer: "Deep", wasFreeform: false });
|
||||
});
|
||||
|
||||
it("returns free-form answers when Copilot allows them", async () => {
|
||||
const params = createParams();
|
||||
const bridge = createCopilotUserInputBridge({ paramsForRun: params });
|
||||
|
||||
const response = bridge.onUserInputRequest(
|
||||
{
|
||||
question: "Which branch?",
|
||||
allowFreeform: true,
|
||||
},
|
||||
{ sessionId: "sdk-session-1" },
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
||||
expect(bridge.handleQueuedMessage("fix/harness-parity")).toBe(true);
|
||||
|
||||
await expect(response).resolves.toEqual({
|
||||
answer: "fix/harness-parity",
|
||||
wasFreeform: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("escapes SDK-controlled prompt text before channel delivery", async () => {
|
||||
const params = createParams();
|
||||
const bridge = createCopilotUserInputBridge({ paramsForRun: params });
|
||||
|
||||
void bridge.onUserInputRequest(
|
||||
{
|
||||
question: "Pick [trusted](https://evil) <@U123> @here\u202e",
|
||||
choices: ["One @everyone", "Two `code`"],
|
||||
allowFreeform: false,
|
||||
},
|
||||
{ sessionId: "sdk-session-1" },
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
||||
const text = expectFirstBlockReplyText(params);
|
||||
expect(text).not.toContain("@here");
|
||||
expect(text).not.toContain("@everyone");
|
||||
expect(text).not.toContain("<@U123>");
|
||||
expect(text).not.toContain("[trusted](https://evil)");
|
||||
expect(text).not.toContain("`code`");
|
||||
expect(text).toContain("\uff20here");
|
||||
expect(text).toContain("\uff3btrusted\uff3d");
|
||||
});
|
||||
|
||||
it("rejects queued messages when no ask_user request is pending", () => {
|
||||
const bridge = createCopilotUserInputBridge({ paramsForRun: createParams() });
|
||||
|
||||
expect(bridge.handleQueuedMessage("late")).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves pending requests with an empty answer when aborted", async () => {
|
||||
const params = createParams();
|
||||
const controller = new AbortController();
|
||||
const bridge = createCopilotUserInputBridge({
|
||||
paramsForRun: params,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const response = bridge.onUserInputRequest(
|
||||
{
|
||||
question: "Continue?",
|
||||
choices: ["Yes", "No"],
|
||||
allowFreeform: false,
|
||||
},
|
||||
{ sessionId: "sdk-session-1" },
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
||||
controller.abort();
|
||||
|
||||
await expect(response).resolves.toEqual({ answer: "", wasFreeform: true });
|
||||
expect(bridge.handleQueuedMessage("1")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
import type { SessionConfig } from "@github/copilot-sdk";
|
||||
import {
|
||||
buildAgentHarnessUserInputAnswers,
|
||||
deliverAgentHarnessUserInputPrompt,
|
||||
embeddedAgentLog,
|
||||
type AgentHarnessUserInputQuestion,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
|
||||
type PendingCopilotUserInput = {
|
||||
question: AgentHarnessUserInputQuestion;
|
||||
resolve: (value: CopilotUserInputResponse) => void;
|
||||
cleanup: () => void;
|
||||
};
|
||||
|
||||
type CopilotUserInputHandler = NonNullable<SessionConfig["onUserInputRequest"]>;
|
||||
type CopilotUserInputRequest = Parameters<CopilotUserInputHandler>[0];
|
||||
type CopilotUserInputResponse = Awaited<ReturnType<CopilotUserInputHandler>>;
|
||||
|
||||
type CopilotUserInputBridge = {
|
||||
onUserInputRequest: CopilotUserInputHandler;
|
||||
handleQueuedMessage: (text: string) => boolean;
|
||||
cancelPending: () => void;
|
||||
};
|
||||
|
||||
const COPILOT_USER_INPUT_QUESTION_ID = "answer";
|
||||
|
||||
export function createCopilotUserInputBridge(params: {
|
||||
paramsForRun: EmbeddedRunAttemptParams;
|
||||
signal?: AbortSignal;
|
||||
}): CopilotUserInputBridge {
|
||||
let pending: PendingCopilotUserInput | undefined;
|
||||
|
||||
const resolvePending = (value: CopilotUserInputResponse) => {
|
||||
const current = pending;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
pending = undefined;
|
||||
current.cleanup();
|
||||
current.resolve(value);
|
||||
};
|
||||
|
||||
return {
|
||||
onUserInputRequest(request) {
|
||||
const question = toQuestion(request);
|
||||
resolvePending(emptyCopilotUserInputResponse());
|
||||
return new Promise<CopilotUserInputResponse>((resolve) => {
|
||||
const abortListener = () => resolvePending(emptyCopilotUserInputResponse());
|
||||
const cleanup = () => params.signal?.removeEventListener("abort", abortListener);
|
||||
pending = { question, resolve, cleanup };
|
||||
params.signal?.addEventListener("abort", abortListener, { once: true });
|
||||
if (params.signal?.aborted) {
|
||||
resolvePending(emptyCopilotUserInputResponse());
|
||||
return;
|
||||
}
|
||||
void deliverAgentHarnessUserInputPrompt(params.paramsForRun, [question], {
|
||||
intro: "Copilot needs input:",
|
||||
formatText: formatCopilotDisplayText,
|
||||
}).catch((error: unknown) => {
|
||||
embeddedAgentLog.warn("failed to deliver copilot user input prompt", { error });
|
||||
});
|
||||
});
|
||||
},
|
||||
handleQueuedMessage(text) {
|
||||
const current = pending;
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
resolvePending(buildCopilotUserInputResponse(current.question, text));
|
||||
return true;
|
||||
},
|
||||
cancelPending() {
|
||||
resolvePending(emptyCopilotUserInputResponse());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toQuestion(request: CopilotUserInputRequest): AgentHarnessUserInputQuestion {
|
||||
return {
|
||||
id: COPILOT_USER_INPUT_QUESTION_ID,
|
||||
header: "Copilot needs input",
|
||||
question: request.question,
|
||||
isOther: request.allowFreeform !== false,
|
||||
isSecret: false,
|
||||
options:
|
||||
request.choices && request.choices.length > 0
|
||||
? request.choices.map((choice: string) => ({ label: choice }))
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCopilotUserInputResponse(
|
||||
question: AgentHarnessUserInputQuestion,
|
||||
inputText: string,
|
||||
): CopilotUserInputResponse {
|
||||
const rawAnswers = buildAgentHarnessUserInputAnswers([question], inputText);
|
||||
const selected = rawAnswers.answers[COPILOT_USER_INPUT_QUESTION_ID]?.answers[0] ?? "";
|
||||
return {
|
||||
answer: selected,
|
||||
wasFreeform: !isChoiceAnswer(question, selected),
|
||||
};
|
||||
}
|
||||
|
||||
function emptyCopilotUserInputResponse(): CopilotUserInputResponse {
|
||||
return { answer: "", wasFreeform: true };
|
||||
}
|
||||
|
||||
function isChoiceAnswer(question: AgentHarnessUserInputQuestion, answer: string): boolean {
|
||||
return Boolean(
|
||||
answer &&
|
||||
question.options?.some((option) => option.label.toLowerCase() === answer.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
function formatCopilotDisplayText(value: string): string {
|
||||
const safe = sanitizeCopilotDisplayText(value).trim();
|
||||
return escapeCopilotChatText(safe || "<unknown>");
|
||||
}
|
||||
|
||||
function sanitizeCopilotDisplayText(value: string): string {
|
||||
let safe = "";
|
||||
for (const character of value) {
|
||||
const codePoint = character.codePointAt(0);
|
||||
safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
function escapeCopilotChatText(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("@", "\uff20")
|
||||
.replaceAll("`", "\uff40")
|
||||
.replaceAll("[", "\uff3b")
|
||||
.replaceAll("]", "\uff3d")
|
||||
.replaceAll("(", "\uff08")
|
||||
.replaceAll(")", "\uff09")
|
||||
.replaceAll("*", "\u2217")
|
||||
.replaceAll("_", "\uff3f")
|
||||
.replaceAll("~", "\uff5e")
|
||||
.replaceAll("|", "\uff5c");
|
||||
}
|
||||
|
||||
function isUnsafeDisplayCodePoint(codePoint: number): boolean {
|
||||
return (
|
||||
codePoint <= 0x001f ||
|
||||
(codePoint >= 0x007f && codePoint <= 0x009f) ||
|
||||
codePoint === 0x00ad ||
|
||||
codePoint === 0x061c ||
|
||||
codePoint === 0x180e ||
|
||||
(codePoint >= 0x200b && codePoint <= 0x200f) ||
|
||||
(codePoint >= 0x202a && codePoint <= 0x202e) ||
|
||||
(codePoint >= 0x2060 && codePoint <= 0x206f) ||
|
||||
codePoint === 0xfeff ||
|
||||
(codePoint >= 0xfff9 && codePoint <= 0xfffb) ||
|
||||
(codePoint >= 0xe0000 && codePoint <= 0xe007f)
|
||||
);
|
||||
}
|
||||
@@ -73,8 +73,6 @@ const GEMINI_FRESHNESS_DAYS: Record<GeminiFreshness, number> = {
|
||||
year: 365,
|
||||
};
|
||||
|
||||
const GEMINI_DAY_FRESHNESS_HINT = "Prioritize web sources published in the last 24 hours.";
|
||||
|
||||
// Gemini's google_search.time_range_filter accepts second-precision RFC 3339
|
||||
// only. Despite the underlying google.protobuf.Timestamp type accepting "0, 3,
|
||||
// 6 or 9 fractional digits", the Search grounding endpoint rejects any
|
||||
@@ -101,18 +99,11 @@ function freshnessStartTime(freshness: GeminiFreshness, now: Date): string {
|
||||
return toGeminiTimeRangeTimestamp(start);
|
||||
}
|
||||
|
||||
function queryWithSoftFreshness(query: string, freshness?: "day"): string {
|
||||
if (freshness !== "day") {
|
||||
return query;
|
||||
}
|
||||
return `${query}\n\nSearch recency instruction: ${GEMINI_DAY_FRESHNESS_HINT} If no matching recent sources are available, state that limitation and use the most relevant available sources.`;
|
||||
}
|
||||
|
||||
function resolveGeminiTimeRangeFilter(
|
||||
args: Record<string, unknown>,
|
||||
now = new Date(),
|
||||
):
|
||||
| { timeRangeFilter?: GeminiTimeRangeFilter; freshness?: "day" }
|
||||
| { timeRangeFilter?: GeminiTimeRangeFilter }
|
||||
| {
|
||||
error:
|
||||
| "invalid_freshness"
|
||||
@@ -142,13 +133,6 @@ function resolveGeminiTimeRangeFilter(
|
||||
|
||||
const { freshness, dateAfter, dateBefore } = parsedTimeFilters;
|
||||
if (freshness) {
|
||||
// Gemini rejects 24-hour google_search.timeRangeFilter windows, while
|
||||
// wider freshness windows still preserve the hard grounding contract.
|
||||
if (freshness === "day") {
|
||||
return {
|
||||
freshness,
|
||||
};
|
||||
}
|
||||
return {
|
||||
timeRangeFilter: {
|
||||
startTime: freshnessStartTime(freshness, now),
|
||||
@@ -337,7 +321,6 @@ export async function executeGeminiSearch(
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
baseUrl,
|
||||
model,
|
||||
timeRange.freshness,
|
||||
timeRange.timeRangeFilter?.startTime,
|
||||
timeRange.timeRangeFilter?.endTime,
|
||||
]);
|
||||
@@ -348,7 +331,7 @@ export async function executeGeminiSearch(
|
||||
|
||||
const start = Date.now();
|
||||
const result = await runGeminiSearch({
|
||||
query: queryWithSoftFreshness(query, timeRange.freshness),
|
||||
query,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
model,
|
||||
|
||||
@@ -40,8 +40,7 @@ const GEMINI_TOOL_PARAMETERS = {
|
||||
language: { type: "string", description: "Not supported by Gemini." },
|
||||
freshness: {
|
||||
type: "string",
|
||||
description:
|
||||
"Filter Gemini search freshness: week, month, and year use hard Google Search time ranges; day prioritizes the last 24 hours as a recency hint.",
|
||||
description: "Limit Google Search grounding to recent results: day, week, month, or year.",
|
||||
},
|
||||
date_after: {
|
||||
type: "string",
|
||||
|
||||
@@ -10,9 +10,10 @@ type TestModelProviderConfig = NonNullable<
|
||||
|
||||
function installGeminiFetch() {
|
||||
const mockFetch = vi.fn((_input?: RequestInfo | URL, _init?: RequestInit) =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
candidates: [
|
||||
{
|
||||
content: { parts: [{ text: "Grounded answer" }] },
|
||||
@@ -22,8 +23,7 @@ function installGeminiFetch() {
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
} as Response),
|
||||
);
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(mockFetch));
|
||||
return mockFetch;
|
||||
@@ -66,7 +66,6 @@ function getGeminiFetchUrl(mockFetch: ReturnType<typeof installGeminiFetch>): st
|
||||
}
|
||||
|
||||
function parseGeminiFetchBody(mockFetch: ReturnType<typeof installGeminiFetch>): {
|
||||
contents?: Array<{ parts?: Array<{ text?: string }> }>;
|
||||
tools?: Array<{ google_search?: { timeRangeFilter?: unknown } }>;
|
||||
} {
|
||||
const [, init] = requireFirstGeminiFetchCall(mockFetch);
|
||||
@@ -75,7 +74,6 @@ function parseGeminiFetchBody(mockFetch: ReturnType<typeof installGeminiFetch>):
|
||||
throw new Error("Expected Gemini fetch body string");
|
||||
}
|
||||
return JSON.parse(body) as {
|
||||
contents?: Array<{ parts?: Array<{ text?: string }> }>;
|
||||
tools?: Array<{ google_search?: { timeRangeFilter?: unknown } }>;
|
||||
};
|
||||
}
|
||||
@@ -479,37 +477,10 @@ describe("google web search provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a soft recency hint for Gemini day freshness shortcuts instead of a 24-hour range", async () => {
|
||||
const mockFetch = installGeminiFetch();
|
||||
const provider = createGeminiWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "AIza-plugin-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
searchConfig: { provider: "gemini" },
|
||||
});
|
||||
|
||||
await tool?.execute({ query: "latest ai news timestamp precision", freshness: "pd" });
|
||||
|
||||
const body = parseGeminiFetchBody(mockFetch);
|
||||
expect(body.tools?.[0]?.google_search?.timeRangeFilter).toBeUndefined();
|
||||
expect(body.contents?.[0]?.parts?.[0]?.text).toContain(
|
||||
"Prioritize web sources published in the last 24 hours.",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves hard Gemini time ranges for wider freshness values", async () => {
|
||||
it("passes freshness to Gemini Google Search grounding as a time range", async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
// Use a wall-clock-realistic moment with non-zero milliseconds; the helper
|
||||
// must strip them to avoid Gemini's "Granularity of nano is not supported".
|
||||
vi.setSystemTime(new Date("2026-04-15T12:00:00.123Z"));
|
||||
const mockFetch = installGeminiFetch();
|
||||
const provider = createGeminiWebSearchProvider();
|
||||
@@ -533,68 +504,13 @@ describe("google web search provider", () => {
|
||||
await tool?.execute({ query: "latest ai news timestamp precision", freshness: "week" });
|
||||
|
||||
const body = parseGeminiFetchBody(mockFetch);
|
||||
expect(body.contents?.[0]?.parts?.[0]?.text).toBe("latest ai news timestamp precision");
|
||||
expect(body.tools?.[0]?.google_search?.timeRangeFilter).toEqual({
|
||||
startTime: "2026-04-08T12:00:00Z",
|
||||
endTime: "2026-04-15T12:00:00Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("partitions Gemini cache entries for soft day freshness, hard week freshness, and no freshness", async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(new Date("2026-04-15T12:00:00.123Z"));
|
||||
const mockFetch = installGeminiFetch();
|
||||
const provider = createGeminiWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "AIza-plugin-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
searchConfig: { provider: "gemini" },
|
||||
});
|
||||
|
||||
await tool?.execute({ query: "same query cache partition", freshness: "day" });
|
||||
await tool?.execute({ query: "same query cache partition", freshness: "week" });
|
||||
await tool?.execute({ query: "same query cache partition" });
|
||||
|
||||
const postCalls = mockFetch.mock.calls.filter(([, init]) => typeof init?.body === "string");
|
||||
expect(postCalls).toHaveLength(3);
|
||||
const parsePostedBody = (call: (typeof postCalls)[number] | undefined) => {
|
||||
const body = call?.[1]?.body;
|
||||
if (typeof body !== "string") {
|
||||
throw new Error("Expected Gemini fetch body to be a string");
|
||||
}
|
||||
return JSON.parse(body) as {
|
||||
contents?: Array<{ parts?: Array<{ text?: string }> }>;
|
||||
tools?: Array<{ google_search?: { timeRangeFilter?: unknown } }>;
|
||||
};
|
||||
};
|
||||
const firstBody = parsePostedBody(postCalls[0]);
|
||||
const secondBody = parsePostedBody(postCalls[1]);
|
||||
const thirdBody = parsePostedBody(postCalls[2]);
|
||||
expect(firstBody.tools?.[0]?.google_search?.timeRangeFilter).toBeUndefined();
|
||||
expect(firstBody.contents?.[0]?.parts?.[0]?.text).toContain(
|
||||
"Prioritize web sources published in the last 24 hours.",
|
||||
);
|
||||
expect(secondBody.tools?.[0]?.google_search?.timeRangeFilter).toEqual({
|
||||
startTime: "2026-04-08T12:00:00Z",
|
||||
endTime: "2026-04-15T12:00:00Z",
|
||||
});
|
||||
expect(secondBody.contents?.[0]?.parts?.[0]?.text).toBe("same query cache partition");
|
||||
expect(thirdBody.tools?.[0]?.google_search?.timeRangeFilter).toBeUndefined();
|
||||
expect(thirdBody.contents?.[0]?.parts?.[0]?.text).toBe("same query cache partition");
|
||||
});
|
||||
|
||||
it("strips sub-second precision from date-range timestamps so Gemini accepts them", async () => {
|
||||
it("strips sub-second precision from freshness timestamps so Gemini accepts them", async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
// "now" with non-zero milliseconds. Without stripping, toISOString() emits
|
||||
// "2026-04-15T12:00:00.123Z", which Gemini's google_search.time_range_filter
|
||||
@@ -619,7 +535,7 @@ describe("google web search provider", () => {
|
||||
searchConfig: { provider: "gemini" },
|
||||
});
|
||||
|
||||
await tool?.execute({ query: "latest ai news", date_after: "2026-04-01" });
|
||||
await tool?.execute({ query: "latest ai news", freshness: "week" });
|
||||
|
||||
const body = parseGeminiFetchBody(mockFetch);
|
||||
const filter = body.tools?.[0]?.google_search?.timeRangeFilter as
|
||||
@@ -628,7 +544,7 @@ describe("google web search provider", () => {
|
||||
expect(filter?.startTime).not.toMatch(/\.\d+Z$/);
|
||||
expect(filter?.endTime).not.toMatch(/\.\d+Z$/);
|
||||
expect(filter).toEqual({
|
||||
startTime: "2026-04-01T00:00:00Z",
|
||||
startTime: "2026-04-08T12:00:00Z",
|
||||
endTime: "2026-04-15T12:00:00Z",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ openclaw plugins install @openclaw/llama-cpp-provider
|
||||
```
|
||||
|
||||
Restart the Gateway after installing or updating the plugin. Use Node 24 for
|
||||
native installs and updates.
|
||||
native installs and updates. Source checkouts leave the native build unapproved
|
||||
by default; run `pnpm approve-builds` and `pnpm rebuild node-llama-cpp` before
|
||||
using local GGUF embeddings from source.
|
||||
|
||||
## Configure
|
||||
|
||||
|
||||
154
extensions/llama-cpp/npm-shrinkwrap.json
generated
154
extensions/llama-cpp/npm-shrinkwrap.json
generated
@@ -7,7 +7,7 @@
|
||||
"": {
|
||||
"name": "@openclaw/llama-cpp-provider",
|
||||
"version": "2026.6.9",
|
||||
"optionalDependencies": {
|
||||
"dependencies": {
|
||||
"node-llama-cpp": "3.18.1"
|
||||
}
|
||||
},
|
||||
@@ -16,7 +16,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz",
|
||||
"integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -26,7 +25,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
@@ -39,7 +37,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
||||
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
@@ -48,8 +45,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
||||
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@node-llama-cpp/linux-arm64": {
|
||||
"version": "3.18.1",
|
||||
@@ -415,15 +411,13 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz",
|
||||
"integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@simple-git/argv-parser": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz",
|
||||
"integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@simple-git/args-pathspec": "^1.0.3"
|
||||
}
|
||||
@@ -433,7 +427,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz",
|
||||
"integrity": "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
},
|
||||
@@ -447,7 +440,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz",
|
||||
"integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
@@ -460,7 +452,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -473,7 +464,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -486,7 +476,6 @@
|
||||
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
|
||||
"integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"retry": "0.13.1"
|
||||
}
|
||||
@@ -496,7 +485,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -506,7 +494,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
@@ -518,15 +505,13 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/chmodrp/-/chmodrp-1.0.2.tgz",
|
||||
"integrity": "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -542,7 +527,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -552,7 +536,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"restore-cursor": "^5.0.0"
|
||||
},
|
||||
@@ -568,7 +551,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
@@ -581,7 +563,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
@@ -596,7 +577,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -606,7 +586,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -616,7 +595,6 @@
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -631,7 +609,6 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -644,7 +621,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz",
|
||||
"integrity": "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"fs-extra": "^11.3.3",
|
||||
@@ -668,7 +644,6 @@
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
@@ -680,15 +655,13 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
@@ -698,7 +671,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -712,15 +684,13 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cross-spawn/node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@@ -736,7 +706,6 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -754,7 +723,6 @@
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
@@ -763,15 +731,13 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/env-var": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz",
|
||||
"integrity": "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -781,7 +747,6 @@
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -790,15 +755,13 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filename-reserved-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
@@ -811,7 +774,6 @@
|
||||
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz",
|
||||
"integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"filename-reserved-regex": "^3.0.0"
|
||||
},
|
||||
@@ -827,7 +789,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz",
|
||||
"integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -842,7 +803,6 @@
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
@@ -852,7 +812,6 @@
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -864,15 +823,13 @@
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
@@ -881,15 +838,13 @@
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipull": {
|
||||
"version": "3.9.5",
|
||||
"resolved": "https://registry.npmjs.org/ipull/-/ipull-3.9.5.tgz",
|
||||
"integrity": "sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tinyhttp/content-disposition": "^2.2.0",
|
||||
"async-retry": "^1.3.3",
|
||||
@@ -929,15 +884,13 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ipull/node_modules/parse-ms": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz",
|
||||
"integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -950,7 +903,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz",
|
||||
"integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"parse-ms": "^3.0.0"
|
||||
},
|
||||
@@ -966,7 +918,6 @@
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"is-fullwidth-code-point": "^5.0.0"
|
||||
@@ -983,7 +934,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.1"
|
||||
},
|
||||
@@ -999,7 +949,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -1012,7 +961,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1025,7 +973,6 @@
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
|
||||
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
@@ -1035,7 +982,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -1047,22 +993,19 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
||||
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-unicode-supported": "^2.0.0",
|
||||
"yoctocolors": "^2.1.1"
|
||||
@@ -1079,7 +1022,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
|
||||
"integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"steno": "^4.0.2"
|
||||
},
|
||||
@@ -1095,7 +1037,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1108,7 +1049,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -1118,7 +1058,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@@ -1128,7 +1067,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
@@ -1140,8 +1078,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.11",
|
||||
@@ -1154,7 +1091,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
@@ -1167,7 +1103,6 @@
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz",
|
||||
"integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
@@ -1176,8 +1111,7 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.9.0.tgz",
|
||||
"integrity": "sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-llama-cpp": {
|
||||
"version": "3.18.1",
|
||||
@@ -1185,7 +1119,6 @@
|
||||
"integrity": "sha512-w0zfuy/IKS2fhrbed5SylZDXJHTVz4HnkwZ4UrFPgSNwJab3QIPwIl4lyCKHHy9flLrtxsAuV5kXfH3HZ6bb8w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@huggingface/jinja": "^0.5.6",
|
||||
"async-retry": "^1.3.3",
|
||||
@@ -1256,7 +1189,6 @@
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mimic-function": "^5.0.0"
|
||||
},
|
||||
@@ -1272,7 +1204,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz",
|
||||
"integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"cli-cursor": "^5.0.0",
|
||||
@@ -1295,7 +1226,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
|
||||
"integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
@@ -1308,7 +1238,6 @@
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1321,7 +1250,6 @@
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1331,7 +1259,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
},
|
||||
@@ -1344,7 +1271,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||
"integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
@@ -1360,7 +1286,6 @@
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
@@ -1372,7 +1297,6 @@
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
@@ -1382,7 +1306,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
@@ -1398,7 +1321,6 @@
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -1408,7 +1330,6 @@
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"onetime": "^7.0.0",
|
||||
"signal-exit": "^4.1.0"
|
||||
@@ -1425,7 +1346,6 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@@ -1438,7 +1358,6 @@
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
@@ -1448,7 +1367,6 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -1461,7 +1379,6 @@
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
@@ -1474,7 +1391,6 @@
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1483,15 +1399,13 @@
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-git": {
|
||||
"version": "3.36.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz",
|
||||
"integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@kwsites/file-exists": "^1.1.1",
|
||||
"@kwsites/promise-deferred": "^1.1.1",
|
||||
@@ -1508,15 +1422,13 @@
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz",
|
||||
"integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
|
||||
"integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.3",
|
||||
"is-fullwidth-code-point": "^5.1.0"
|
||||
@@ -1533,7 +1445,6 @@
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz",
|
||||
"integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1546,7 +1457,6 @@
|
||||
"resolved": "https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz",
|
||||
"integrity": "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^6.2.0",
|
||||
"ansi-styles": "^6.2.1",
|
||||
@@ -1561,15 +1471,13 @@
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stdout-update/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
@@ -1587,7 +1495,6 @@
|
||||
"resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz",
|
||||
"integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1600,7 +1507,6 @@
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
|
||||
"integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"strip-ansi": "^7.1.2"
|
||||
@@ -1617,7 +1523,6 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
@@ -1633,7 +1538,6 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -1643,7 +1547,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
|
||||
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
@@ -1660,7 +1563,6 @@
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -1669,15 +1571,13 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/validate-npm-package-name": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz",
|
||||
"integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
@@ -1687,7 +1587,6 @@
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
|
||||
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"isexe": "^4.0.0"
|
||||
},
|
||||
@@ -1703,7 +1602,6 @@
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
@@ -1721,7 +1619,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1731,7 +1628,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
@@ -1747,7 +1643,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1757,7 +1652,6 @@
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -1772,7 +1666,6 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -1785,7 +1678,6 @@
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -1795,7 +1687,6 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -1805,7 +1696,6 @@
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
@@ -1824,7 +1714,6 @@
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -1834,7 +1723,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1844,7 +1732,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1854,7 +1741,6 @@
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -1869,7 +1755,6 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -1882,7 +1767,6 @@
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"url": "https://github.com/openclaw/openclaw"
|
||||
},
|
||||
"type": "module",
|
||||
"optionalDependencies": {
|
||||
"dependencies": {
|
||||
"node-llama-cpp": "3.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -59,84 +59,6 @@ describe("mattermost monitor gating", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("processes engaged thread follow-ups without a mention", () => {
|
||||
const resolveRequireMention = vi.fn(() => true);
|
||||
|
||||
expect(
|
||||
evaluateMattermostMentionGate({
|
||||
kind: "channel",
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
channelId: "chan-1",
|
||||
resolveRequireMention,
|
||||
wasMentioned: false,
|
||||
threadAlreadyEngaged: true,
|
||||
isControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
oncharEnabled: false,
|
||||
oncharTriggered: false,
|
||||
canDetectMention: true,
|
||||
}),
|
||||
).toEqual({
|
||||
shouldRequireMention: true,
|
||||
shouldBypassMention: false,
|
||||
effectiveWasMentioned: true,
|
||||
dropReason: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("engaged threads respond even when onchar is enabled but not triggered", () => {
|
||||
const resolveRequireMention = vi.fn(() => true);
|
||||
|
||||
expect(
|
||||
evaluateMattermostMentionGate({
|
||||
kind: "channel",
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
channelId: "chan-1",
|
||||
resolveRequireMention,
|
||||
wasMentioned: false,
|
||||
threadAlreadyEngaged: true,
|
||||
isControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
oncharEnabled: true,
|
||||
oncharTriggered: false,
|
||||
canDetectMention: true,
|
||||
}),
|
||||
).toEqual({
|
||||
shouldRequireMention: true,
|
||||
shouldBypassMention: false,
|
||||
effectiveWasMentioned: true,
|
||||
dropReason: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("drops non-mentioned channel traffic outside an engaged thread", () => {
|
||||
const resolveRequireMention = vi.fn(() => true);
|
||||
|
||||
expect(
|
||||
evaluateMattermostMentionGate({
|
||||
kind: "channel",
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
channelId: "chan-1",
|
||||
resolveRequireMention,
|
||||
wasMentioned: false,
|
||||
threadAlreadyEngaged: false,
|
||||
isControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
oncharEnabled: false,
|
||||
oncharTriggered: false,
|
||||
canDetectMention: true,
|
||||
}),
|
||||
).toEqual({
|
||||
shouldRequireMention: true,
|
||||
shouldBypassMention: false,
|
||||
effectiveWasMentioned: false,
|
||||
dropReason: "missing-mention",
|
||||
});
|
||||
});
|
||||
|
||||
it("bypasses mention for authorized control commands and allows direct chats", () => {
|
||||
const resolveRequireMention = vi.fn(() => true);
|
||||
|
||||
|
||||
@@ -43,9 +43,6 @@ export type MattermostMentionGateInput = {
|
||||
requireMentionOverride?: boolean;
|
||||
resolveRequireMention: (params: MattermostRequireMentionResolverInput) => boolean;
|
||||
wasMentioned: boolean;
|
||||
// Bot has already replied in this thread; treat follow-ups as addressed so the
|
||||
// user need not re-mention on every turn (parity with Slack thread participation).
|
||||
threadAlreadyEngaged?: boolean;
|
||||
isControlCommand: boolean;
|
||||
commandAuthorized: boolean;
|
||||
oncharEnabled: boolean;
|
||||
@@ -78,16 +75,12 @@ export function evaluateMattermostMentionGate(
|
||||
!params.wasMentioned &&
|
||||
params.commandAuthorized;
|
||||
const effectiveWasMentioned =
|
||||
params.wasMentioned ||
|
||||
shouldBypassMention ||
|
||||
params.oncharTriggered ||
|
||||
params.threadAlreadyEngaged === true;
|
||||
params.wasMentioned || shouldBypassMention || params.oncharTriggered;
|
||||
if (
|
||||
params.oncharEnabled &&
|
||||
!params.oncharTriggered &&
|
||||
!params.wasMentioned &&
|
||||
!params.isControlCommand &&
|
||||
params.threadAlreadyEngaged !== true
|
||||
!params.isControlCommand
|
||||
) {
|
||||
return {
|
||||
shouldRequireMention,
|
||||
|
||||
@@ -371,7 +371,6 @@ describe("deliverMattermostReplyWithDraftPreview", () => {
|
||||
it("suppresses reasoning-prefixed finals before preview finalization", async () => {
|
||||
const draftStream = createDraftStreamMock();
|
||||
const deliverFinal = vi.fn(async () => {});
|
||||
const recordThreadParticipation = vi.fn();
|
||||
|
||||
await deliverMattermostReplyWithDraftPreview({
|
||||
payload: { text: " \n > Reasoning:\n> _hidden_" } as never,
|
||||
@@ -383,7 +382,6 @@ describe("deliverMattermostReplyWithDraftPreview", () => {
|
||||
resolvePreviewFinalText: (text) => text?.trim(),
|
||||
previewState: { finalizedViaPreviewPost: false },
|
||||
logVerboseMessage: vi.fn(),
|
||||
recordThreadParticipation,
|
||||
deliverPayload: deliverFinal,
|
||||
});
|
||||
|
||||
@@ -392,36 +390,6 @@ describe("deliverMattermostReplyWithDraftPreview", () => {
|
||||
expect(draftStream.discardPending).not.toHaveBeenCalled();
|
||||
expect(draftStream.clear).not.toHaveBeenCalled();
|
||||
expect(updateMattermostPostSpy).not.toHaveBeenCalled();
|
||||
// No visible reply was sent, so the thread must not be marked as participated.
|
||||
expect(recordThreadParticipation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("records thread participation when a same-thread final finalizes the preview in place", async () => {
|
||||
const draftStream = createDraftStreamMock();
|
||||
const deliverFinal = vi.fn(async () => {});
|
||||
const recordThreadParticipation = vi.fn();
|
||||
|
||||
await deliverMattermostReplyWithDraftPreview({
|
||||
payload: { text: "All good" } as never,
|
||||
info: { kind: "final" },
|
||||
kind: "channel",
|
||||
client: createMattermostClientMock(),
|
||||
draftStream,
|
||||
effectiveReplyToId: "thread-root-1",
|
||||
resolvePreviewFinalText: (text) => text?.trim(),
|
||||
previewState: { finalizedViaPreviewPost: false },
|
||||
logVerboseMessage: vi.fn(),
|
||||
recordThreadParticipation,
|
||||
deliverPayload: deliverFinal,
|
||||
});
|
||||
|
||||
// Default streaming finalizes by editing the preview post, bypassing deliverPayload —
|
||||
// participation must still be recorded (regression: PR #95552 review P1).
|
||||
expect(updateMattermostPostSpy).toHaveBeenCalledWith(expect.anything(), "preview-post-1", {
|
||||
message: "All good",
|
||||
});
|
||||
expect(deliverFinal).not.toHaveBeenCalled();
|
||||
expect(recordThreadParticipation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("deletes the preview after a successful normal final send", async () => {
|
||||
|
||||
@@ -116,10 +116,6 @@ import {
|
||||
import { sendMessageMattermost } from "./send.js";
|
||||
import { cleanupSlashCommands } from "./slash-commands.js";
|
||||
import { deactivateSlashCommands, getSlashCommandState } from "./slash-state.js";
|
||||
import {
|
||||
hasMattermostThreadParticipationWithPersistence,
|
||||
recordMattermostThreadParticipation,
|
||||
} from "./thread-participation.js";
|
||||
|
||||
export {
|
||||
evaluateMattermostMentionGate,
|
||||
@@ -331,10 +327,6 @@ type MattermostDraftPreviewDeliverParams = {
|
||||
previewState: MattermostDraftPreviewState;
|
||||
logVerboseMessage: (message: string) => void;
|
||||
deliverPayload: (payload: ReplyPayload) => Promise<void>;
|
||||
// Visible same-thread finals can be delivered by editing the draft preview in
|
||||
// place (onPreviewFinalized) without ever calling deliverPayload; this lets the
|
||||
// caller record thread participation on that path too.
|
||||
recordThreadParticipation?: () => void;
|
||||
};
|
||||
|
||||
export async function deliverMattermostReplyWithDraftPreview(
|
||||
@@ -382,9 +374,6 @@ export async function deliverMattermostReplyWithDraftPreview(
|
||||
},
|
||||
onPreviewFinalized: () => {
|
||||
params.previewState.finalizedViaPreviewPost = true;
|
||||
// The visible final reply landed by editing the preview post, so the normal
|
||||
// deliverPayload record path is skipped; record participation explicitly here.
|
||||
params.recordThreadParticipation?.();
|
||||
},
|
||||
buildSupplementalPayload: (payload) =>
|
||||
getReplyPayloadTtsSupplement(payload) ? buildTtsSupplementMediaPayload(payload) : undefined,
|
||||
@@ -1495,16 +1484,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
: { triggered: false, stripped: rawText };
|
||||
const oncharTriggered = oncharResult.triggered;
|
||||
const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
|
||||
// Threads the bot already replied in auto-engage: follow-ups resume without
|
||||
// a re-mention even under requireMention. Keyed by the thread root id.
|
||||
const threadAlreadyEngaged =
|
||||
kind !== "direct" && effectiveReplyToId
|
||||
? await hasMattermostThreadParticipationWithPersistence({
|
||||
accountId: account.accountId,
|
||||
channelId,
|
||||
threadRootId: effectiveReplyToId,
|
||||
})
|
||||
: false;
|
||||
const mentionDecision = evaluateMattermostMentionGate({
|
||||
kind,
|
||||
cfg,
|
||||
@@ -1514,7 +1493,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
requireMentionOverride: account.requireMention,
|
||||
resolveRequireMention: core.channel.groups.resolveRequireMention,
|
||||
wasMentioned,
|
||||
threadAlreadyEngaged,
|
||||
isControlCommand,
|
||||
commandAuthorized,
|
||||
oncharEnabled,
|
||||
@@ -1803,18 +1781,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
if (info.kind === "final") {
|
||||
progressDraft.markFinalReplyStarted();
|
||||
}
|
||||
// A visible same-thread final arrives either via a normal send or by editing
|
||||
// the draft preview in place; record participation on whichever path fires.
|
||||
const markThreadParticipation = () => {
|
||||
if (kind !== "direct" && effectiveReplyToId) {
|
||||
recordMattermostThreadParticipation(
|
||||
account.accountId,
|
||||
channelId,
|
||||
effectiveReplyToId,
|
||||
{ agentId: route.agentId },
|
||||
);
|
||||
}
|
||||
};
|
||||
await deliverMattermostReplyWithDraftPreview({
|
||||
payload: payloadEntry,
|
||||
info,
|
||||
@@ -1825,7 +1791,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
resolvePreviewFinalText,
|
||||
previewState,
|
||||
logVerboseMessage,
|
||||
recordThreadParticipation: markThreadParticipation,
|
||||
deliverPayload: async (payloadToDeliver) => {
|
||||
const outcome = await deliverMattermostReplyPayload({
|
||||
core,
|
||||
@@ -1844,11 +1809,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
sendMessage: sendMessageMattermost,
|
||||
onDmChannelResolution: deliveryBarrier.trackDmChannelResolution,
|
||||
});
|
||||
// Record only on a visible send so threads we merely observed
|
||||
// (reasoning-only/empty/suppressed) do not auto-engage later.
|
||||
if (outcome === "text" || outcome === "media") {
|
||||
markThreadParticipation();
|
||||
}
|
||||
const deliveryLog = formatMattermostFinalDeliveryOutcomeLog({
|
||||
outcome,
|
||||
payload: payloadToDeliver,
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
// Mattermost tests cover thread participation cache plugin behavior.
|
||||
import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import {
|
||||
createPluginStateKeyedStoreForTests,
|
||||
resetPluginStateStoreForTests,
|
||||
} from "openclaw/plugin-sdk/plugin-state-test-runtime";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { setMattermostRuntime } from "../runtime.js";
|
||||
import {
|
||||
clearMattermostThreadParticipationCache,
|
||||
hasMattermostThreadParticipationWithPersistence,
|
||||
recordMattermostThreadParticipation,
|
||||
} from "./thread-participation.js";
|
||||
|
||||
// Drain microtasks + the immediate queue so the fire-and-forget persistent write
|
||||
// in recordMattermostThreadParticipation has settled before we assert on it.
|
||||
const flush = (): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
|
||||
function setRuntime(openKeyedStore: (options: OpenKeyedStoreOptions) => unknown): void {
|
||||
setMattermostRuntime({
|
||||
state: { openKeyedStore },
|
||||
logging: { getChildLogger: () => ({ warn() {} }) },
|
||||
} as unknown as PluginRuntime);
|
||||
}
|
||||
|
||||
function setPersistentRuntime(): void {
|
||||
setRuntime((options) => createPluginStateKeyedStoreForTests("mattermost", options));
|
||||
}
|
||||
|
||||
describe("mattermost thread participation", () => {
|
||||
beforeEach(() => {
|
||||
resetPluginStateStoreForTests();
|
||||
clearMattermostThreadParticipationCache();
|
||||
setPersistentRuntime();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearMattermostThreadParticipationCache();
|
||||
resetPluginStateStoreForTests();
|
||||
});
|
||||
|
||||
it("remembers a thread the bot replied in", async () => {
|
||||
recordMattermostThreadParticipation("acct", "chan", "root-1");
|
||||
await expect(
|
||||
hasMattermostThreadParticipationWithPersistence({
|
||||
accountId: "acct",
|
||||
channelId: "chan",
|
||||
threadRootId: "root-1",
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("isolates participation by account, channel, and thread", async () => {
|
||||
recordMattermostThreadParticipation("acct", "chan", "root-1");
|
||||
await flush();
|
||||
for (const probe of [
|
||||
{ accountId: "other", channelId: "chan", threadRootId: "root-1" },
|
||||
{ accountId: "acct", channelId: "other", threadRootId: "root-1" },
|
||||
{ accountId: "acct", channelId: "chan", threadRootId: "root-2" },
|
||||
]) {
|
||||
await expect(hasMattermostThreadParticipationWithPersistence(probe)).resolves.toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores empty identifiers", async () => {
|
||||
recordMattermostThreadParticipation("", "chan", "root-1");
|
||||
await expect(
|
||||
hasMattermostThreadParticipationWithPersistence({
|
||||
accountId: "",
|
||||
channelId: "chan",
|
||||
threadRootId: "root-1",
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("recovers participation from the persistent store after the in-memory cache is lost", async () => {
|
||||
recordMattermostThreadParticipation("acct", "chan", "root-1");
|
||||
await flush();
|
||||
// Simulate a restart: in-memory cache cleared, persistent SQLite store intact.
|
||||
clearMattermostThreadParticipationCache();
|
||||
await expect(
|
||||
hasMattermostThreadParticipationWithPersistence({
|
||||
accountId: "acct",
|
||||
channelId: "chan",
|
||||
threadRootId: "root-1",
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("degrades to in-memory only when the persistent store fails", async () => {
|
||||
setRuntime(() => {
|
||||
throw new Error("sqlite unavailable");
|
||||
});
|
||||
// record + read must not throw; the in-memory cache still answers.
|
||||
recordMattermostThreadParticipation("acct", "chan", "root-1");
|
||||
await expect(
|
||||
hasMattermostThreadParticipationWithPersistence({
|
||||
accountId: "acct",
|
||||
channelId: "chan",
|
||||
threadRootId: "root-1",
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
hasMattermostThreadParticipationWithPersistence({
|
||||
accountId: "acct",
|
||||
channelId: "chan",
|
||||
threadRootId: "missing",
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,151 +0,0 @@
|
||||
// Mattermost plugin module implements thread participation cache behavior.
|
||||
import { resolveGlobalDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
||||
import { getOptionalMattermostRuntime } from "../runtime.js";
|
||||
|
||||
/**
|
||||
* In-memory + persisted cache of Mattermost threads the bot has replied in.
|
||||
* Lets the bot auto-respond to thread follow-ups without a re-mention after its
|
||||
* first visible reply. Mirrors the Slack `sent-thread-cache` dual-layer pattern.
|
||||
*/
|
||||
|
||||
const TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const MAX_ENTRIES = 5000;
|
||||
const PERSISTENT_MAX_ENTRIES = 1000;
|
||||
const PERSISTENT_NAMESPACE = "mattermost.thread-participation";
|
||||
|
||||
type MattermostThreadParticipationRecord = {
|
||||
agentId?: string;
|
||||
repliedAt: number;
|
||||
};
|
||||
|
||||
type MattermostThreadParticipationStore = {
|
||||
register(
|
||||
key: string,
|
||||
value: MattermostThreadParticipationRecord,
|
||||
opts?: { ttlMs?: number },
|
||||
): Promise<void>;
|
||||
lookup(key: string): Promise<MattermostThreadParticipationRecord | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep thread participation shared across bundled chunks so thread auto-reply
|
||||
* gating does not diverge between the inbound-gate and reply-dispatch paths.
|
||||
*/
|
||||
const MATTERMOST_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.mattermostThreadParticipation");
|
||||
const threadParticipation = resolveGlobalDedupeCache(MATTERMOST_THREAD_PARTICIPATION_KEY, {
|
||||
ttlMs: TTL_MS,
|
||||
maxSize: MAX_ENTRIES,
|
||||
});
|
||||
|
||||
let persistentStore: MattermostThreadParticipationStore | undefined;
|
||||
let persistentStoreDisabled = false;
|
||||
|
||||
function makeKey(accountId: string, channelId: string, threadRootId: string): string {
|
||||
return `${accountId}:${channelId}:${threadRootId}`;
|
||||
}
|
||||
|
||||
function reportPersistentThreadParticipationError(error: unknown): void {
|
||||
try {
|
||||
getOptionalMattermostRuntime()
|
||||
?.logging.getChildLogger({ plugin: "mattermost", feature: "thread-participation-state" })
|
||||
.warn("Mattermost persistent thread participation state failed", { error: String(error) });
|
||||
} catch {
|
||||
// Best effort only: persistent state must never break Mattermost message handling.
|
||||
}
|
||||
}
|
||||
|
||||
function disablePersistentThreadParticipation(error: unknown): void {
|
||||
persistentStoreDisabled = true;
|
||||
persistentStore = undefined;
|
||||
reportPersistentThreadParticipationError(error);
|
||||
}
|
||||
|
||||
function getPersistentThreadParticipationStore(): MattermostThreadParticipationStore | undefined {
|
||||
if (persistentStoreDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
if (persistentStore) {
|
||||
return persistentStore;
|
||||
}
|
||||
const runtime = getOptionalMattermostRuntime();
|
||||
if (!runtime) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
persistentStore = runtime.state.openKeyedStore<MattermostThreadParticipationRecord>({
|
||||
namespace: PERSISTENT_NAMESPACE,
|
||||
maxEntries: PERSISTENT_MAX_ENTRIES,
|
||||
defaultTtlMs: TTL_MS,
|
||||
});
|
||||
return persistentStore;
|
||||
} catch (error) {
|
||||
disablePersistentThreadParticipation(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function rememberPersistentThreadParticipation(params: { key: string; agentId?: string }): void {
|
||||
const store = getPersistentThreadParticipationStore();
|
||||
if (!store) {
|
||||
return;
|
||||
}
|
||||
void store
|
||||
.register(params.key, {
|
||||
// Stored for future per-agent thread routing; current reads only need presence.
|
||||
...(params.agentId ? { agentId: params.agentId } : {}),
|
||||
repliedAt: Date.now(),
|
||||
})
|
||||
.catch(disablePersistentThreadParticipation);
|
||||
}
|
||||
|
||||
async function lookupPersistentThreadParticipation(key: string): Promise<boolean> {
|
||||
const store = getPersistentThreadParticipationStore();
|
||||
if (!store) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return Boolean(await store.lookup(key));
|
||||
} catch (error) {
|
||||
disablePersistentThreadParticipation(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function recordMattermostThreadParticipation(
|
||||
accountId: string,
|
||||
channelId: string,
|
||||
threadRootId: string,
|
||||
opts?: { agentId?: string },
|
||||
): void {
|
||||
if (!accountId || !channelId || !threadRootId) {
|
||||
return;
|
||||
}
|
||||
const key = makeKey(accountId, channelId, threadRootId);
|
||||
threadParticipation.check(key);
|
||||
rememberPersistentThreadParticipation({ key, agentId: opts?.agentId });
|
||||
}
|
||||
|
||||
export async function hasMattermostThreadParticipationWithPersistence(params: {
|
||||
accountId: string;
|
||||
channelId: string;
|
||||
threadRootId: string;
|
||||
}): Promise<boolean> {
|
||||
if (!params.accountId || !params.channelId || !params.threadRootId) {
|
||||
return false;
|
||||
}
|
||||
const key = makeKey(params.accountId, params.channelId, params.threadRootId);
|
||||
if (threadParticipation.peek(key)) {
|
||||
return true;
|
||||
}
|
||||
const found = await lookupPersistentThreadParticipation(key);
|
||||
if (found) {
|
||||
threadParticipation.check(key);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export function clearMattermostThreadParticipationCache(): void {
|
||||
threadParticipation.clear();
|
||||
persistentStore = undefined;
|
||||
persistentStoreDisabled = false;
|
||||
}
|
||||
@@ -2,12 +2,9 @@
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
|
||||
|
||||
const {
|
||||
setRuntime: setMattermostRuntime,
|
||||
getRuntime: getMattermostRuntime,
|
||||
tryGetRuntime: getOptionalMattermostRuntime,
|
||||
} = createPluginRuntimeStore<PluginRuntime>({
|
||||
pluginId: "mattermost",
|
||||
errorMessage: "Mattermost runtime not initialized",
|
||||
});
|
||||
export { getMattermostRuntime, getOptionalMattermostRuntime, setMattermostRuntime };
|
||||
const { setRuntime: setMattermostRuntime, getRuntime: getMattermostRuntime } =
|
||||
createPluginRuntimeStore<PluginRuntime>({
|
||||
pluginId: "mattermost",
|
||||
errorMessage: "Mattermost runtime not initialized",
|
||||
});
|
||||
export { getMattermostRuntime, setMattermostRuntime };
|
||||
|
||||
@@ -559,85 +559,6 @@ describe("compileMemoryWikiVault", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("excludes concept and synthesis pages from stale-pages report", async () => {
|
||||
const { rootDir, config } = await createVault({
|
||||
rootDir: nextCaseRoot(),
|
||||
initialize: true,
|
||||
});
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "entities", "entity-alpha.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
pageType: "entity",
|
||||
id: "entity.alpha",
|
||||
title: "Alpha Entity",
|
||||
sourceIds: ["source.alpha"],
|
||||
updatedAt: "2025-06-01T00:00:00.000Z",
|
||||
},
|
||||
body: "# Alpha Entity\n",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "sources", "source-alpha.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
pageType: "source",
|
||||
id: "source.alpha",
|
||||
title: "Alpha Source",
|
||||
updatedAt: "2025-06-01T00:00:00.000Z",
|
||||
},
|
||||
body: "# Alpha Source\n",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// Concept page with old updatedAt — should be excluded from stale-pages
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "concepts", "concept-beta.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
pageType: "concept",
|
||||
id: "concept.beta",
|
||||
title: "Beta Concept",
|
||||
sourceIds: ["source.alpha"],
|
||||
updatedAt: "2025-06-01T00:00:00.000Z",
|
||||
},
|
||||
body: "# Beta Concept\n",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// Synthesis page with old updatedAt — should be excluded from stale-pages
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "syntheses", "synthesis-gamma.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
pageType: "synthesis",
|
||||
id: "synthesis.gamma",
|
||||
title: "Gamma Synthesis",
|
||||
sourceIds: ["source.alpha"],
|
||||
updatedAt: "2025-06-01T00:00:00.000Z",
|
||||
},
|
||||
body: "# Gamma Synthesis\n",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await compileMemoryWikiVault(config);
|
||||
|
||||
const stalePages = await fs.readFile(path.join(rootDir, "reports", "stale-pages.md"), "utf8");
|
||||
|
||||
// Entity and source pages still appear in stale-pages
|
||||
expect(stalePages).toContain("[Alpha Entity](../entities/entity-alpha.md)");
|
||||
expect(stalePages).toContain("[Alpha Source](../sources/source-alpha.md)");
|
||||
// Concept and synthesis pages are excluded
|
||||
expect(stalePages).not.toContain("[Beta Concept](../concepts/concept-beta.md)");
|
||||
expect(stalePages).not.toContain("[Gamma Synthesis](../syntheses/synthesis-gamma.md)");
|
||||
});
|
||||
|
||||
it("skips dashboard report pages when createDashboards is disabled", async () => {
|
||||
const { rootDir, config } = await createVault({
|
||||
rootDir: nextCaseRoot(),
|
||||
|
||||
@@ -214,9 +214,6 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
|
||||
.filter(
|
||||
(page) =>
|
||||
page.kind !== "report" &&
|
||||
// concept/synthesis are intentionally durable references
|
||||
page.kind !== "concept" &&
|
||||
page.kind !== "synthesis" &&
|
||||
!(
|
||||
isUnmanagedRawSourceSummary(page) &&
|
||||
!managedImportedSourcePagePaths.has(page.relativePath)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"@openclaw/slack": "workspace:*",
|
||||
"@openclaw/whatsapp": "workspace:*",
|
||||
"@openclaw/crabline": "0.1.0",
|
||||
"crabline": "github:openclaw/crabline#b3513f66053788c6a7bd2bc76fbfc7201f647d29",
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Qa Lab plugin helper creates collision-resistant artifact run identifiers.
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export function createQaArtifactRunId(): string {
|
||||
return `${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
||||
}
|
||||
@@ -255,39 +255,6 @@ describe("runQaCharacterEval", () => {
|
||||
expect(report).not.toContain("Judge Raw Reply");
|
||||
});
|
||||
|
||||
it("creates a unique default output directory under repo artifacts", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
outputDir: params.outputDir,
|
||||
model: params.primaryModel,
|
||||
transcript: "USER Alice: hi\n\nASSISTANT openclaw: default dir reply",
|
||||
}),
|
||||
);
|
||||
const runJudge = makeRunJudge([
|
||||
{
|
||||
model: "openai/gpt-5.5",
|
||||
rank: 1,
|
||||
score: 8,
|
||||
summary: "solid",
|
||||
strengths: ["clear"],
|
||||
weaknesses: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await runQaCharacterEval({
|
||||
repoRoot: tempRoot,
|
||||
models: ["openai/gpt-5.5"],
|
||||
runSuite,
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(path.dirname(result.outputDir)).toBe(path.join(tempRoot, ".artifacts", "qa-e2e"));
|
||||
expect(path.basename(result.outputDir)).toMatch(
|
||||
/^character-eval-[a-z0-9]+-[a-f0-9]{8}$/u,
|
||||
);
|
||||
await expect(fs.stat(result.reportPath).then((stats) => stats.isFile())).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("can hide candidate model refs from judge prompts and map rankings back", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
|
||||
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { createQaArtifactRunId } from "./artifact-run-id.js";
|
||||
import { isQaFastModeModelRef, type QaProviderMode } from "./model-selection.js";
|
||||
import {
|
||||
QA_FRONTIER_CHARACTER_EVAL_MODELS,
|
||||
@@ -521,7 +520,7 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
|
||||
const outputDir =
|
||||
params.outputDir ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `character-eval-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `character-eval-${Date.now().toString(36)}`);
|
||||
const runsDir = path.join(outputDir, "runs");
|
||||
await fs.mkdir(runsDir, { recursive: true });
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
||||
import {
|
||||
OPENCLAW_CRABLINE_DEFAULT_CHANNEL,
|
||||
resolveOpenClawCrablineChannelDriverSelection,
|
||||
} from "@openclaw/crabline";
|
||||
} from "crabline";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
type QaRuntimeParitySuiteSummary,
|
||||
} from "./agentic-parity-report.js";
|
||||
import { resolveQaParityPackScenarioIds } from "./agentic-parity.js";
|
||||
import { createQaArtifactRunId } from "./artifact-run-id.js";
|
||||
import { runQaCharacterEval, type QaCharacterModelOptions } from "./character-eval.js";
|
||||
import { resolveRepoRelativeOutputDir } from "./cli-paths.js";
|
||||
import {
|
||||
@@ -408,7 +407,7 @@ async function runQaParityPreflight(params: {
|
||||
".artifacts",
|
||||
"qa-e2e",
|
||||
"preflight",
|
||||
`suite-${createQaArtifactRunId()}`,
|
||||
`suite-${Date.now().toString(36)}`,
|
||||
);
|
||||
const result = await runQaSuiteWithInfraRetry(() =>
|
||||
runQaFlowSuiteFromRuntime({
|
||||
@@ -1057,7 +1056,7 @@ export async function runQaParityReportCommand(opts: {
|
||||
}
|
||||
const outputDir =
|
||||
resolveRepoRelativeOutputDir(repoRoot, opts.outputDir) ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `parity-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `parity-${Date.now().toString(36)}`);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
if (opts.runtimeAxis === true) {
|
||||
@@ -1150,7 +1149,7 @@ export async function runQaConfidenceReportCommand(opts: {
|
||||
const artifactRoot = path.resolve(repoRoot, opts.artifactRoot ?? ".");
|
||||
const outputDir =
|
||||
resolveRepoRelativeOutputDir(repoRoot, opts.outputDir) ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `confidence-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `confidence-${Date.now().toString(36)}`);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
const manifest = await readQaConfidenceManifestFile(manifestPath);
|
||||
const reportPayload = await buildQaConfidenceReport({
|
||||
@@ -1179,7 +1178,7 @@ export async function runQaConfidenceSelfTestCommand(opts: {
|
||||
const repoRoot = path.resolve(opts.repoRoot ?? process.cwd());
|
||||
const outputDir =
|
||||
resolveRepoRelativeOutputDir(repoRoot, opts.outputDir) ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `confidence-self-test-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `confidence-self-test-${Date.now().toString(36)}`);
|
||||
const result = await writeQaConfidenceSelfTestArtifacts({ outputDir });
|
||||
process.stdout.write(`QA confidence self-test report: ${result.reportPath}\n`);
|
||||
process.stdout.write(`QA confidence self-test summary: ${result.summaryPath}\n`);
|
||||
@@ -1269,7 +1268,7 @@ export async function runQaJsonlReplayCommand(opts: {
|
||||
const transcriptDir = path.resolve(repoRoot, opts.transcripts ?? "qa/scenarios/jsonl-replay");
|
||||
const outputDir =
|
||||
resolveRepoRelativeOutputDir(repoRoot, opts.outputDir) ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `jsonl-replay-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `jsonl-replay-${Date.now().toString(36)}`);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
const result = await runJsonlReplay(
|
||||
{
|
||||
|
||||
@@ -800,33 +800,6 @@ describe("qa cli registration", () => {
|
||||
await expect(invalidProgram.parseAsync(["node", "openclaw", ...args])).rejects.toThrow(message);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[["qa", "ui", "--port", "65536"], "--port must be a TCP port between 1 and 65535."],
|
||||
[
|
||||
["qa", "ui", "--advertise-port", "999999"],
|
||||
"--advertise-port must be a TCP port between 1 and 65535.",
|
||||
],
|
||||
[
|
||||
["qa", "docker-scaffold", "--output-dir", "/tmp/qa", "--gateway-port", "65536"],
|
||||
"--gateway-port must be a TCP port between 1 and 65535.",
|
||||
],
|
||||
[
|
||||
["qa", "up", "--qa-lab-port", "65536"],
|
||||
"--qa-lab-port must be a TCP port between 1 and 65535.",
|
||||
],
|
||||
[["qa", "aimock", "--port", "65536"], "--port must be a TCP port between 1 and 65535."],
|
||||
])("rejects out-of-range QA port option %j", async (args, message) => {
|
||||
const invalidProgram = new Command();
|
||||
invalidProgram.exitOverride();
|
||||
invalidProgram.configureOutput({
|
||||
writeErr: () => {},
|
||||
writeOut: () => {},
|
||||
});
|
||||
registerQaLabCli(invalidProgram);
|
||||
|
||||
await expect(invalidProgram.parseAsync(["node", "openclaw", ...args])).rejects.toThrow(message);
|
||||
});
|
||||
|
||||
it("shows an enable hint when a discovered runner plugin is installed but blocked", async () => {
|
||||
listQaRunnerCliContributions.mockReset().mockReturnValue([createBlockedQaRunnerContribution()]);
|
||||
const blockedProgram = new Command();
|
||||
|
||||
@@ -62,7 +62,6 @@ const QA_RUN_PROFILE_ONLY_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
const QA_RUN_SELF_CHECK_ONLY_OPTIONS = [{ optionName: "output", flag: "--output" }] as const;
|
||||
const MAX_QA_CLI_TCP_PORT = 65_535;
|
||||
|
||||
type QaSuiteCliOptions = QaScenarioRunCliOptions & {
|
||||
channelDriver?: QaSuiteCommandOptions["channelDriver"];
|
||||
@@ -106,14 +105,6 @@ function parseQaCliPositiveIntegerOption(value: string, flag: string): number {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseQaCliTcpPortOption(value: string, flag: string): number {
|
||||
const parsed = parseQaCliPositiveIntegerOption(value, flag);
|
||||
if (parsed > MAX_QA_CLI_TCP_PORT) {
|
||||
throw invalidQaCliArgument(`${flag} must be a TCP port between 1 and 65535.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseQaEvidenceModeOption(value: string): QaProfileCommandOptions["evidenceMode"] {
|
||||
const evidenceMode = value.trim();
|
||||
if (evidenceMode === "full" || evidenceMode === "slim") {
|
||||
@@ -876,11 +867,11 @@ export function registerQaLabCli(program: Command) {
|
||||
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
||||
.option("--host <host>", "Bind host", "127.0.0.1")
|
||||
.option("--port <port>", "Bind port", (value: string) =>
|
||||
parseQaCliTcpPortOption(value, "--port"),
|
||||
parseQaCliPositiveIntegerOption(value, "--port"),
|
||||
)
|
||||
.option("--advertise-host <host>", "Optional public host to advertise in bootstrap payloads")
|
||||
.option("--advertise-port <port>", "Optional public port to advertise", (value: string) =>
|
||||
parseQaCliTcpPortOption(value, "--advertise-port"),
|
||||
parseQaCliPositiveIntegerOption(value, "--advertise-port"),
|
||||
)
|
||||
.option("--control-ui-url <url>", "Optional Control UI URL to embed beside the QA panel")
|
||||
.option(
|
||||
@@ -918,10 +909,10 @@ export function registerQaLabCli(program: Command) {
|
||||
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
||||
.requiredOption("--output-dir <path>", "Output directory for docker-compose + state files")
|
||||
.option("--gateway-port <port>", "Gateway host port", (value: string) =>
|
||||
parseQaCliTcpPortOption(value, "--gateway-port"),
|
||||
parseQaCliPositiveIntegerOption(value, "--gateway-port"),
|
||||
)
|
||||
.option("--qa-lab-port <port>", "QA lab host port", (value: string) =>
|
||||
parseQaCliTcpPortOption(value, "--qa-lab-port"),
|
||||
parseQaCliPositiveIntegerOption(value, "--qa-lab-port"),
|
||||
)
|
||||
.option("--provider-base-url <url>", "Provider base URL for the QA gateway")
|
||||
.option("--image <name>", "Prebaked image name", "openclaw:qa-local-prebaked")
|
||||
@@ -959,10 +950,10 @@ export function registerQaLabCli(program: Command) {
|
||||
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
||||
.option("--output-dir <path>", "Output directory for docker-compose + state files")
|
||||
.option("--gateway-port <port>", "Gateway host port", (value: string) =>
|
||||
parseQaCliTcpPortOption(value, "--gateway-port"),
|
||||
parseQaCliPositiveIntegerOption(value, "--gateway-port"),
|
||||
)
|
||||
.option("--qa-lab-port <port>", "QA lab host port", (value: string) =>
|
||||
parseQaCliTcpPortOption(value, "--qa-lab-port"),
|
||||
parseQaCliPositiveIntegerOption(value, "--qa-lab-port"),
|
||||
)
|
||||
.option("--provider-base-url <url>", "Provider base URL for the QA gateway")
|
||||
.option("--image <name>", "Image tag", "openclaw:qa-local-prebaked")
|
||||
@@ -994,7 +985,7 @@ export function registerQaLabCli(program: Command) {
|
||||
.description(providerCommand.description)
|
||||
.option("--host <host>", "Bind host", "127.0.0.1")
|
||||
.option("--port <port>", "Bind port", (value: string) =>
|
||||
parseQaCliTcpPortOption(value, "--port"),
|
||||
parseQaCliPositiveIntegerOption(value, "--port"),
|
||||
)
|
||||
.action(async (opts: { host?: string; port?: number }) => {
|
||||
await runQaProviderServer(providerCommand.providerMode, opts);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Qa Lab tests cover Crabline fake-provider transport integration behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { OPENCLAW_CRABLINE_MANIFEST_PATH } from "@openclaw/crabline";
|
||||
import { OPENCLAW_CRABLINE_MANIFEST_PATH } from "crabline";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { withTempDir } from "openclaw/plugin-sdk/test-env";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
startOpenClawCrablineAdapter,
|
||||
type OpenClawCrablineChannelDriverSelection,
|
||||
type StartedOpenClawCrablineAdapter,
|
||||
} from "@openclaw/crabline";
|
||||
} from "crabline";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
@@ -101,10 +101,7 @@ async function postCrablineInbound(params: {
|
||||
url: params.adapter.manifest.endpoints.adminInboundUrl,
|
||||
init: {
|
||||
body: JSON.stringify(params.providerBody),
|
||||
headers: {
|
||||
authorization: `Bearer ${params.adapter.manifest.adminToken}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
headers: { "content-type": "application/json" },
|
||||
method: "POST",
|
||||
},
|
||||
policy: { allowPrivateNetwork: true },
|
||||
|
||||
@@ -1231,9 +1231,6 @@ describe("buildQaRuntimeEnv", () => {
|
||||
await expect(readFile(path.join(artifactDir, "README.txt"), "utf8")).resolves.toContain(
|
||||
"was not copied because it may contain credentials or auth tokens",
|
||||
);
|
||||
await expect(readFile(path.join(artifactDir, "README.txt"), "utf8")).resolves.not.toContain(
|
||||
tempRoot,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects preserved gateway artifacts outside the repo root", async () => {
|
||||
|
||||
@@ -162,7 +162,7 @@ async function preserveQaGatewayDebugArtifacts(params: {
|
||||
[
|
||||
"Only sanitized gateway debug artifacts are preserved here.",
|
||||
"The full QA gateway runtime was not copied because it may contain credentials or auth tokens.",
|
||||
"Original runtime temp root omitted because local temp paths can identify the runner.",
|
||||
`Original runtime temp root: ${params.tempRoot}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
|
||||
@@ -657,8 +657,7 @@ describe("qa-lab server", () => {
|
||||
});
|
||||
|
||||
const result = await lab.runSelfCheck();
|
||||
expect(path.dirname(result.outputPath)).toBe(path.join(repoRoot, ".artifacts", "qa-e2e"));
|
||||
expect(path.basename(result.outputPath)).toMatch(/^self-check-[a-z0-9]+-[a-f0-9]{8}\.md$/u);
|
||||
expect(result.outputPath).toBe(path.join(repoRoot, ".artifacts", "qa-e2e", "self-check.md"));
|
||||
expect(await readFile(result.outputPath, "utf8")).toContain("Synthetic Slack-class roundtrip");
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtim
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { chromium } from "playwright-core";
|
||||
import { z } from "zod";
|
||||
import { createQaArtifactRunId } from "../../artifact-run-id.js";
|
||||
import { QA_EVIDENCE_FILENAME, buildLiveTransportEvidenceSummary } from "../../evidence-summary.js";
|
||||
import { startQaGatewayChild } from "../../gateway-child.js";
|
||||
import { isTruthyOptIn } from "../../mantis-options.runtime.js";
|
||||
@@ -1528,7 +1527,7 @@ export async function runDiscordQaLive(params: {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const outputDir =
|
||||
params.outputDir ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `discord-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `discord-${Date.now().toString(36)}`);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const providerMode = normalizeQaProviderMode(
|
||||
|
||||
@@ -9,7 +9,6 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { z } from "zod";
|
||||
import { createQaArtifactRunId } from "../../artifact-run-id.js";
|
||||
import { QA_EVIDENCE_FILENAME, buildLiveTransportEvidenceSummary } from "../../evidence-summary.js";
|
||||
import { startQaGatewayChild } from "../../gateway-child.js";
|
||||
import { isTruthyOptIn } from "../../mantis-options.runtime.js";
|
||||
@@ -1712,7 +1711,7 @@ export async function runSlackQaLive(params: {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const outputDir =
|
||||
params.outputDir ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `slack-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `slack-${Date.now().toString(36)}`);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const providerMode = normalizeQaProviderMode(
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { isRecord, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { z } from "zod";
|
||||
import { createQaArtifactRunId } from "../../artifact-run-id.js";
|
||||
import {
|
||||
QA_EVIDENCE_FILENAME,
|
||||
buildLiveTransportEvidenceSummary,
|
||||
@@ -1806,7 +1805,7 @@ export async function runTelegramQaLive(params: {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const outputDir =
|
||||
params.outputDir ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `telegram-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `telegram-${Date.now().toString(36)}`);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const providerMode = normalizeQaProviderMode(
|
||||
|
||||
@@ -15,7 +15,6 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
||||
import { z } from "zod";
|
||||
import { createQaArtifactRunId } from "../../artifact-run-id.js";
|
||||
import { QA_EVIDENCE_FILENAME, buildLiveTransportEvidenceSummary } from "../../evidence-summary.js";
|
||||
import { startQaGatewayChild } from "../../gateway-child.js";
|
||||
import { isTruthyOptIn } from "../../mantis-options.runtime.js";
|
||||
@@ -3034,7 +3033,7 @@ export async function runWhatsAppQaLive(params: {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const outputDir =
|
||||
params.outputDir ??
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `whatsapp-${createQaArtifactRunId()}`);
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", `whatsapp-${Date.now().toString(36)}`);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const providerMode = normalizeQaProviderMode(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { access, mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawCrablineChannelDriverSelection } from "@openclaw/crabline";
|
||||
import type { OpenClawCrablineChannelDriverSelection } from "crabline";
|
||||
import { sleep } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { appendRegularFile } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
@@ -3314,7 +3314,7 @@ describe("qa mock openai server", () => {
|
||||
const toolPlanOutput = outputItem(await response.json());
|
||||
expect(toolPlanOutput.type).toBe("function_call");
|
||||
expect(toolPlanOutput.name).toBe("web_search");
|
||||
expect(String(toolPlanOutput.arguments)).toContain("OPENCLAW_QA_WEB_SEARCH_DENIED_INPUT");
|
||||
expect(String(toolPlanOutput.arguments)).toContain("denied-input");
|
||||
});
|
||||
|
||||
it("plans QA subagent handoff calls even when Codex dynamic tools are not in body.tools", async () => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { escapeRegExp } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { readRequestBodyWithLimit } from "openclaw/plugin-sdk/webhook-ingress";
|
||||
import { closeQaHttpServer } from "../../bus-server.js";
|
||||
import { QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY } from "../../qa-web-search-provider.js";
|
||||
import { writeJson } from "../shared/http-json.js";
|
||||
|
||||
type ResponsesInputItem = Record<string, unknown>;
|
||||
@@ -861,9 +860,6 @@ function extractToolSearchTarget(text: string): string | null {
|
||||
}
|
||||
|
||||
function buildQaToolSearchArgs(targetTool: string, failureMode: boolean): Record<string, unknown> {
|
||||
if (failureMode && targetTool === "web_search") {
|
||||
return { query: QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY };
|
||||
}
|
||||
if (failureMode) {
|
||||
return { __qaFailureMode: "denied-input" };
|
||||
}
|
||||
@@ -1539,57 +1535,49 @@ function buildToolCallEvents(prompt: string): StreamEvent[] {
|
||||
function buildReleaseAuditJson() {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
verified: false,
|
||||
verified: true,
|
||||
findings: [
|
||||
{
|
||||
id: "REL-GATEWAY-417",
|
||||
source: "src/gateway/reconnect.ts",
|
||||
status: "retry jitter verified, resume token fallback still needs manual spot check",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: "REL-CHANNEL-238",
|
||||
source: "src/channels/delivery.ts",
|
||||
status: "thread replies preserve ordering, root-channel fallback needs handoff note",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: "REL-CRON-904",
|
||||
source: "src/scheduling/cron.ts",
|
||||
status: "single-run lock verified for restart wakeups",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: "REL-MEMORY-552",
|
||||
source: "src/memory/recall.ts",
|
||||
status:
|
||||
"fallback summary survives empty memory search; ranking sample needs second reviewer",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: "REL-PLUGIN-319",
|
||||
source: "src/plugins/runtime.ts",
|
||||
status: "bundled runtime manifest loads cleanly after restart",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: "REL-INSTALL-846",
|
||||
source: "install/update.ts",
|
||||
status: "update smoke passed from previous stable tag",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: "REL-DOCS-611",
|
||||
source: "docs/operator-notes.md",
|
||||
status:
|
||||
"docs mention reconnect, cron, memory, plugin, and installer checks; channel ordering and UI notes need maintainer handoff",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: "REL-UI-BLOCKED",
|
||||
source: "ui/control-panel.ts",
|
||||
status: "blocked: source file was referenced by checklist but missing from the fixture",
|
||||
verified: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
|
||||
import { createQaLabWebSearchProvider as createQaLabWebSearchContractProvider } from "../web-search-contract-api.js";
|
||||
import {
|
||||
createQaLabWebSearchProvider,
|
||||
QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY,
|
||||
QA_LAB_WEB_SEARCH_PROVIDER_ID,
|
||||
} from "./qa-web-search-provider.js";
|
||||
|
||||
@@ -56,16 +55,4 @@ describe("qa-lab web search provider", () => {
|
||||
|
||||
await expect(tool.execute({ __qaFailureMode: "denied-input" })).rejects.toThrow(/query/i);
|
||||
});
|
||||
|
||||
it("keeps the QA failure sentinel as a deterministic tool failure", async () => {
|
||||
const provider = createQaLabWebSearchProvider();
|
||||
const tool = provider.createTool({});
|
||||
if (!tool) {
|
||||
throw new Error("expected QA Lab web search tool");
|
||||
}
|
||||
|
||||
await expect(tool.execute({ query: QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY })).rejects.toThrow(
|
||||
/denied input sentinel/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
|
||||
export const QA_LAB_WEB_SEARCH_PROVIDER_ID = "qa-lab-search";
|
||||
export const QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY = "OPENCLAW_QA_WEB_SEARCH_DENIED_INPUT";
|
||||
|
||||
const QaLabWebSearchSchema = {
|
||||
type: "object",
|
||||
@@ -65,9 +64,6 @@ export function createQaLabWebSearchProvider(): WebSearchProviderPlugin {
|
||||
parameters: QaLabWebSearchSchema,
|
||||
execute: async (args) => {
|
||||
const query = readStringParam(args, "query", { required: true });
|
||||
if (query === QA_LAB_WEB_SEARCH_DENIED_INPUT_QUERY) {
|
||||
throw new Error("QA Lab web_search denied input sentinel");
|
||||
}
|
||||
const count =
|
||||
readPositiveIntegerParam(args, "count", {
|
||||
max: MAX_SEARCH_COUNT,
|
||||
|
||||
@@ -161,22 +161,6 @@ describe("qa run config", () => {
|
||||
expect(outputDir.startsWith(path.join(repoRoot, ".artifacts", "qa-e2e", "lab-"))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps generated run output dirs unique within the same millisecond", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-06-23T07:30:00.000Z"));
|
||||
const repoRoot = path.resolve("/tmp/openclaw-repo");
|
||||
const first = createQaRunOutputDir(repoRoot);
|
||||
const second = createQaRunOutputDir(repoRoot);
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
expect(path.basename(first)).toMatch(/^lab-2026-06-23-073000000Z-[0-9a-f]{8}$/u);
|
||||
expect(path.basename(second)).toMatch(/^lab-2026-06-23-073000000Z-[0-9a-f]{8}$/u);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers the Codex OAuth default when the runtime resolver says it is available", () => {
|
||||
defaultQaRuntimeModelForMode.mockImplementation((mode, options) =>
|
||||
mode === "live-frontier"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Qa Lab helper module supports run config behavior.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { defaultQaModelForMode as defaultStaticQaModelForMode } from "./model-selection.js";
|
||||
@@ -133,5 +132,5 @@ export function createIdleQaRunnerSnapshot(scenarios: QaSeedScenario[]): QaLabRu
|
||||
|
||||
export function createQaRunOutputDir(baseDir = process.cwd()) {
|
||||
const stamp = new Date().toISOString().replaceAll(":", "").replaceAll(".", "").replace("T", "-");
|
||||
return path.join(baseDir, ".artifacts", "qa-e2e", `lab-${stamp}-${randomUUID().slice(0, 8)}`);
|
||||
return path.join(baseDir, ".artifacts", "qa-e2e", `lab-${stamp}`);
|
||||
}
|
||||
|
||||
@@ -611,7 +611,7 @@ describe("qa scenario catalog", () => {
|
||||
"this seeded scenario is mock-openai only",
|
||||
);
|
||||
expect(heartbeatFlow).toContain("sessionKey");
|
||||
expect(heartbeatFlow).toContain("commitmentOutbound.length === 0");
|
||||
expect(heartbeatFlow).toContain("targetOutbound.length === 0");
|
||||
expect(heartbeatFlow).not.toContain("waitForNoOutbound");
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export const QA_MATURITY_TAXONOMY_PATH = "taxonomy.yaml";
|
||||
export const QA_MATURITY_SCORES_PATH = "qa/maturity-scores.yaml";
|
||||
export const QA_MATURITY_SCORE_KEYS = ["quality", "completeness"] as const;
|
||||
export const QA_MATURITY_SCORE_LABELS = [
|
||||
"Clawesome",
|
||||
"Lovable",
|
||||
"Stable",
|
||||
"Beta",
|
||||
"Alpha",
|
||||
|
||||
@@ -53,13 +53,10 @@ describe("resolveQaSelfCheckOutputPath", () => {
|
||||
).toBe("/tmp/custom/self-check.md");
|
||||
});
|
||||
|
||||
it("anchors default self-check reports under unique files in the provided repo root", () => {
|
||||
it("anchors default self-check reports under the provided repo root", () => {
|
||||
const repoRoot = path.resolve("/tmp/openclaw-repo");
|
||||
const firstPath = resolveQaSelfCheckOutputPath({ repoRoot });
|
||||
const secondPath = resolveQaSelfCheckOutputPath({ repoRoot });
|
||||
|
||||
expect(path.dirname(firstPath)).toBe(path.join(repoRoot, ".artifacts", "qa-e2e"));
|
||||
expect(path.basename(firstPath)).toMatch(/^self-check-[a-z0-9]+-[a-f0-9]{8}\.md$/u);
|
||||
expect(secondPath).not.toBe(firstPath);
|
||||
expect(resolveQaSelfCheckOutputPath({ repoRoot })).toBe(
|
||||
path.join(repoRoot, ".artifacts", "qa-e2e", "self-check.md"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { renderQaMarkdownReport } from "openclaw/plugin-sdk/qa-runtime";
|
||||
import { createQaArtifactRunId } from "./artifact-run-id.js";
|
||||
import type { QaBusState } from "./bus-state.js";
|
||||
import { createQaTransportAdapter, type QaTransportId } from "./qa-transport-registry.js";
|
||||
import { runQaScenario, type QaScenarioResult } from "./scenario.js";
|
||||
@@ -28,7 +27,7 @@ export function resolveQaSelfCheckOutputPath(params?: { outputPath?: string; rep
|
||||
return params.outputPath;
|
||||
}
|
||||
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
|
||||
return path.join(repoRoot, ".artifacts", "qa-e2e", `self-check-${createQaArtifactRunId()}.md`);
|
||||
return path.join(repoRoot, ".artifacts", "qa-e2e", "self-check.md");
|
||||
}
|
||||
|
||||
export async function runQaSelfCheckAgainstState(params: {
|
||||
|
||||
@@ -28,19 +28,23 @@ async function makeTempRepo(prefix: string) {
|
||||
return repoRoot;
|
||||
}
|
||||
|
||||
async function writeEvidence(pathLocal: string, writeFile = true) {
|
||||
const evidence = {
|
||||
kind: "openclaw.qa.evidence-summary",
|
||||
schemaVersion: 2,
|
||||
generatedAt: "2026-06-14T00:00:00.000Z",
|
||||
evidenceMode: "full",
|
||||
entries: [],
|
||||
};
|
||||
if (writeFile) {
|
||||
await fs.mkdir(path.dirname(pathLocal), { recursive: true });
|
||||
await fs.writeFile(pathLocal, `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
|
||||
}
|
||||
return evidence;
|
||||
async function writeEvidence(pathLocal: string) {
|
||||
await fs.mkdir(path.dirname(pathLocal), { recursive: true });
|
||||
await fs.writeFile(
|
||||
pathLocal,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
kind: "openclaw.qa.evidence-summary",
|
||||
schemaVersion: 2,
|
||||
generatedAt: "2026-06-14T00:00:00.000Z",
|
||||
evidenceMode: "full",
|
||||
entries: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
describe("qa suite runtime launcher", () => {
|
||||
@@ -48,17 +52,12 @@ describe("qa suite runtime launcher", () => {
|
||||
runQaFlowSuite.mockReset();
|
||||
runQaTestFileScenarios.mockReset();
|
||||
runQaFlowSuite.mockImplementation(
|
||||
async (
|
||||
params:
|
||||
| { outputDir?: string; scenarioIds?: string[]; writeEvidenceFile?: boolean }
|
||||
| undefined,
|
||||
) => {
|
||||
async (params: { outputDir?: string; scenarioIds?: string[] } | undefined) => {
|
||||
const outputDir = params?.outputDir ?? "/tmp/qa-flow";
|
||||
const evidencePath = path.join(outputDir, "qa-evidence.json");
|
||||
const evidence = await writeEvidence(evidencePath, params?.writeEvidenceFile);
|
||||
await writeEvidence(evidencePath);
|
||||
const scenarioIds = params?.scenarioIds ?? ["channel-chat-baseline"];
|
||||
return {
|
||||
evidence,
|
||||
outputDir,
|
||||
evidencePath,
|
||||
reportPath: path.join(outputDir, "qa-suite-report.md"),
|
||||
@@ -77,16 +76,14 @@ describe("qa suite runtime launcher", () => {
|
||||
async (params: {
|
||||
outputDir: string;
|
||||
scenarios: Array<{ id: string; execution: { kind: "script" | "vitest" | "playwright" } }>;
|
||||
writeEvidenceFile?: boolean;
|
||||
}) => {
|
||||
const [scenario] = params.scenarios;
|
||||
if (!scenario) {
|
||||
throw new Error("expected scenario");
|
||||
}
|
||||
const evidencePath = path.join(params.outputDir, "qa-evidence.json");
|
||||
const evidence = await writeEvidence(evidencePath, params.writeEvidenceFile);
|
||||
await writeEvidence(evidencePath);
|
||||
return {
|
||||
evidence,
|
||||
outputDir: params.outputDir,
|
||||
executionKind: scenario.execution.kind,
|
||||
evidencePath,
|
||||
@@ -250,27 +247,15 @@ describe("qa suite runtime launcher", () => {
|
||||
expect.objectContaining({
|
||||
outputDir: path.join(outputDir, "flow"),
|
||||
scenarioIds: ["channel-chat-baseline"],
|
||||
writeEvidenceFile: false,
|
||||
}),
|
||||
);
|
||||
expect(runQaTestFileScenarios).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
outputDir: path.join(outputDir, "playwright"),
|
||||
writeEvidenceFile: false,
|
||||
}),
|
||||
);
|
||||
await expect(fs.access(path.join(outputDir, "qa-suite-summary.json"))).resolves.toBeUndefined();
|
||||
await expect(fs.access(path.join(outputDir, "qa-evidence.json"))).resolves.toBeUndefined();
|
||||
await expect(fs.access(path.join(outputDir, "flow", "qa-evidence.json"))).rejects.toMatchObject(
|
||||
{
|
||||
code: "ENOENT",
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
fs.access(path.join(outputDir, "playwright", "qa-evidence.json")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
const summary = JSON.parse(
|
||||
await fs.readFile(path.join(outputDir, "qa-suite-summary.json"), "utf8"),
|
||||
) as {
|
||||
|
||||
@@ -175,7 +175,6 @@ async function runQaTestFileSuiteFromRuntime(params: {
|
||||
providerMode,
|
||||
primaryModel,
|
||||
scenarios: params.scenarios,
|
||||
writeEvidenceFile: runParams?.writeEvidenceFile,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -293,13 +292,6 @@ async function readQaSuiteEvidenceSummary(evidencePath: string) {
|
||||
return validateQaEvidenceSummaryJson(JSON.parse(await fs.readFile(evidencePath, "utf8")));
|
||||
}
|
||||
|
||||
async function resolveQaSuiteResultEvidenceSummary(result: {
|
||||
evidence?: QaEvidenceSummaryJson;
|
||||
evidencePath: string;
|
||||
}) {
|
||||
return result.evidence ?? (await readQaSuiteEvidenceSummary(result.evidencePath));
|
||||
}
|
||||
|
||||
function mergeQaEvidenceSummaries(params: {
|
||||
evidenceSummaries: readonly QaEvidenceSummaryJson[];
|
||||
generatedAt: string;
|
||||
@@ -497,7 +489,6 @@ async function runUnifiedQaSuite(params: {
|
||||
flowPartitions.length === 1
|
||||
? suitePartitionOutputDir(outputDir, "flow")
|
||||
: flowSuitePartitionOutputDir(outputDir, partition.kind),
|
||||
writeEvidenceFile: false,
|
||||
providerMode,
|
||||
primaryModel,
|
||||
alternateModel,
|
||||
@@ -521,7 +512,7 @@ async function runUnifiedQaSuite(params: {
|
||||
}
|
||||
}
|
||||
return {
|
||||
evidenceSummaries: [await resolveQaSuiteResultEvidenceSummary(result)],
|
||||
evidenceSummaries: [await readQaSuiteEvidenceSummary(result.evidencePath)],
|
||||
scenarioResults,
|
||||
};
|
||||
},
|
||||
@@ -539,14 +530,13 @@ async function runUnifiedQaSuite(params: {
|
||||
runParams: {
|
||||
...params.runParams,
|
||||
outputDir: suitePartitionOutputDir(outputDir, kind),
|
||||
writeEvidenceFile: false,
|
||||
providerMode,
|
||||
primaryModel,
|
||||
scenarioIds: testFileScenarios.map((scenario) => scenario.id),
|
||||
},
|
||||
scenarios: testFileScenarios,
|
||||
});
|
||||
testFileEvidenceSummaries.push(await resolveQaSuiteResultEvidenceSummary(result));
|
||||
testFileEvidenceSummaries.push(await readQaSuiteEvidenceSummary(result.evidencePath));
|
||||
testFileScenarioResults.push(
|
||||
...result.results.map((scenarioResult) => ({
|
||||
scenarioId: scenarioResult.scenario.id,
|
||||
|
||||
@@ -86,22 +86,6 @@ describe("qa suite planning helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("creates unique default suite output dirs inside the repo root", async () => {
|
||||
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-suite-default-root-"));
|
||||
try {
|
||||
const firstDir = await resolveQaSuiteOutputDir(repoRoot);
|
||||
const secondDir = await resolveQaSuiteOutputDir(repoRoot);
|
||||
|
||||
expect(path.dirname(firstDir)).toBe(path.join(repoRoot, ".artifacts", "qa-e2e"));
|
||||
expect(path.basename(firstDir)).toMatch(/^suite-[a-z0-9]+-[a-f0-9]{8}$/u);
|
||||
expect(secondDir).not.toBe(firstDir);
|
||||
await expect(lstat(firstDir).then((stats) => stats.isDirectory())).resolves.toBe(true);
|
||||
await expect(lstat(secondDir).then((stats) => stats.isDirectory())).resolves.toBe(true);
|
||||
} finally {
|
||||
await rm(repoRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects symlinked suite output dirs that escape the repo root", async () => {
|
||||
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-suite-root-"));
|
||||
const outsideRoot = await mkdtemp(path.join(os.tmpdir(), "qa-suite-outside-"));
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import path from "node:path";
|
||||
import { parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { createQaArtifactRunId } from "./artifact-run-id.js";
|
||||
import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "./cli-paths.js";
|
||||
import type { QaCliBackendAuthMode } from "./gateway-child.js";
|
||||
import { splitQaModelRef as splitModelRef, type QaProviderMode } from "./model-selection.js";
|
||||
@@ -339,7 +338,7 @@ async function mapQaSuiteWithConcurrency<T, U>(
|
||||
|
||||
async function resolveQaSuiteOutputDir(repoRoot: string, outputDir?: string) {
|
||||
const targetDir = !outputDir
|
||||
? path.join(repoRoot, ".artifacts", "qa-e2e", `suite-${createQaArtifactRunId()}`)
|
||||
? path.join(repoRoot, ".artifacts", "qa-e2e", `suite-${Date.now().toString(36)}`)
|
||||
: outputDir;
|
||||
if (!path.isAbsolute(targetDir)) {
|
||||
const resolved = resolveRepoRelativeOutputDir(repoRoot, targetDir);
|
||||
|
||||
@@ -274,36 +274,6 @@ describe("qa suite", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("can return evidence without writing duplicate child evidence files", async () => {
|
||||
const outputDir = await tempDirs.makeTempDir("qa-suite-artifacts-memory-evidence-");
|
||||
try {
|
||||
const artifacts = await qaSuiteProgressTesting.writeQaSuiteArtifacts({
|
||||
outputDir,
|
||||
startedAt: new Date("2026-04-11T00:00:00.000Z"),
|
||||
finishedAt: new Date("2026-04-11T00:01:00.000Z"),
|
||||
scenarios: [{ name: "Baseline", status: "pass", steps: [] }],
|
||||
scenarioDefinitions: [makeQaSuiteTestScenario("baseline")],
|
||||
transport: {
|
||||
id: "qa-channel",
|
||||
createReportNotes: () => [],
|
||||
} as unknown as QaTransportAdapter,
|
||||
providerMode: "mock-openai",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
alternateModel: "mock-openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
concurrency: 1,
|
||||
writeEvidenceFile: false,
|
||||
});
|
||||
|
||||
expect(artifacts.evidence?.kind).toBe(QA_EVIDENCE_SUMMARY_KIND);
|
||||
await expect(fs.access(artifacts.evidencePath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(fs.access(artifacts.reportPath)).resolves.toBeUndefined();
|
||||
await expect(fs.access(artifacts.summaryPath)).resolves.toBeUndefined();
|
||||
} finally {
|
||||
await fs.rm(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("writes Crabline channel-driver smoke artifacts when selected", async () => {
|
||||
const outputDir = await tempDirs.makeTempDir("qa-suite-crabline-");
|
||||
try {
|
||||
@@ -463,7 +433,6 @@ describe("qa suite", () => {
|
||||
enabledPluginIds: ["acpx"],
|
||||
transportReadyTimeoutMs: 180_000,
|
||||
forcedRuntime: "codex",
|
||||
writeEvidenceFile: false,
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
@@ -476,7 +445,6 @@ describe("qa suite", () => {
|
||||
enabledPluginIds: ["acpx"],
|
||||
transportReadyTimeoutMs: 180_000,
|
||||
forcedRuntime: "codex",
|
||||
writeEvidenceFile: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user