Merge branch 'main' into fix/claw-mounts

This commit is contained in:
Ken Bolton
2026-03-25 11:13:16 -04:00
committed by GitHub
23 changed files with 577 additions and 757 deletions

View File

@@ -50,7 +50,7 @@ Already configured. Continue.
**Verify:** `git remote -v` should show `origin` → user's repo, `upstream``qwibitai/nanoclaw.git`.
## 1. Bootstrap (Node.js + Dependencies)
## 1. Bootstrap (Node.js + Dependencies + OneCLI)
Run `bash setup.sh` and parse the status block.
@@ -62,6 +62,34 @@ Run `bash setup.sh` and parse the status block.
- If NATIVE_OK=false → better-sqlite3 failed to load. Install build tools and re-run.
- Record PLATFORM and IS_WSL for later steps.
After bootstrap succeeds, install OneCLI and its CLI tool:
```bash
curl -fsSL onecli.sh/install | sh
curl -fsSL onecli.sh/cli/install | sh
```
Verify both installed: `onecli version`. If the command is not found, the CLI was likely installed to `~/.local/bin/`. Add it to PATH for the current session and persist it:
```bash
export PATH="$HOME/.local/bin:$PATH"
# Persist for future sessions (append to shell profile if not already present)
grep -q '.local/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
grep -q '.local/bin' ~/.zshrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
```
Then re-verify with `onecli version`.
Point the CLI at the local OneCLI instance (it defaults to the cloud service otherwise):
```bash
onecli config set api-host http://127.0.0.1:10254
```
Ensure `.env` has the OneCLI URL (create the file if it doesn't exist):
```bash
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env
```
## 2. Check Environment
Run `npx tsx setup/index.ts --step environment` and parse the status block.
@@ -70,6 +98,13 @@ Run `npx tsx setup/index.ts --step environment` and parse the status block.
- If HAS_REGISTERED_GROUPS=true → note existing config, offer to skip or reconfigure
- Record APPLE_CONTAINER and DOCKER values for step 3
## 2a. Timezone
Run `npx tsx setup/index.ts --step timezone` and parse the status block.
- If NEEDS_USER_INPUT=true → The system timezone could not be autodetected (e.g. POSIX-style TZ like `IST-2`). AskUserQuestion: "What is your timezone?" with common options (America/New_York, Europe/London, Asia/Jerusalem, Asia/Tokyo) and an "Other" escape. Then re-run: `npx tsx setup/index.ts --step timezone -- --tz <their-answer>`.
- If STATUS=success → Timezone is configured. Note RESOLVED_TZ for reference.
## 3. Container Runtime
### 3a. Choose runtime
@@ -112,15 +147,47 @@ Run `npx tsx setup/index.ts --step container -- --runtime <chosen>` and parse th
**If TEST_OK=false but BUILD_OK=true:** The image built but won't run. Check logs — common cause is runtime not fully started. Wait a moment and retry the test.
## 4. Claude Authentication (No Script)
## 4. Anthropic Credentials via OneCLI
If HAS_ENV=true from step 2, read `.env` and check for `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`. If present, confirm with user: keep or reconfigure?
NanoClaw uses OneCLI to manage credentials — API keys are never stored in `.env` or exposed to containers. The OneCLI gateway injects them at request time.
AskUserQuestion: Claude subscription (Pro/Max) vs Anthropic API key?
Check if a secret already exists:
```bash
onecli secrets list
```
**Subscription:** Tell user to run `claude setup-token` in another terminal, copy the token, add `CLAUDE_CODE_OAUTH_TOKEN=<token>` to `.env`. Do NOT collect the token in chat.
If an Anthropic secret is listed, confirm with user: keep or reconfigure? If keeping, skip to step 5.
**API key:** Tell user to add `ANTHROPIC_API_KEY=<key>` to `.env`.
AskUserQuestion: Do you want to use your **Claude subscription** (Pro/Max) or an **Anthropic API key**?
1. **Claude subscription (Pro/Max)** — description: "Uses your existing Claude Pro or Max subscription. You'll run `claude setup-token` in another terminal to get your token."
2. **Anthropic API key** — description: "Pay-per-use API key from console.anthropic.com."
### Subscription path
Tell the user to run `claude setup-token` in another terminal and copy the token it outputs. Do NOT collect the token in chat.
Once they have the token, they register it with OneCLI. AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_TOKEN --host-pattern api.anthropic.com`"
### API key path
Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one.
Then AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`"
### After either path
Ask them to let you know when done.
**If the user's response happens to contain a token or key** (starts with `sk-ant-`): handle it gracefully — run the `onecli secrets create` command with that value on their behalf.
**After user confirms:** verify with `onecli secrets list` that an Anthropic secret exists. If not, ask again.
## 5. Set Up Channels
@@ -198,7 +265,7 @@ Run `npx tsx setup/index.ts --step verify` and parse the status block.
**If STATUS=failed, fix each:**
- SERVICE=stopped → `npm run build`, then restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) or `bash start-nanoclaw.sh` (WSL nohup)
- SERVICE=not_found → re-run step 7
- CREDENTIALS=missing → re-run step 4
- CREDENTIALS=missing → re-run step 4 (check `onecli secrets list` for Anthropic secret)
- CHANNEL_AUTH shows `not_found` for any channel → re-invoke that channel's skill (e.g. `/add-telegram`)
- REGISTERED_GROUPS=0 → re-invoke the channel skills from step 5
- MOUNT_ALLOWLIST=missing → `npx tsx setup/index.ts --step mounts -- --empty`
@@ -207,7 +274,7 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/
## Troubleshooting
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), missing `.env` (step 4), missing channel credentials (re-invoke channel skill).
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), OneCLI not running (check `curl http://127.0.0.1:10254/api/health`), missing channel credentials (re-invoke channel skill).
**Container agent fails ("Claude Code process exited with code 1"):** Ensure the container runtime is running — `open -a Docker` (macOS Docker), `container system start` (Apple Container), or `sudo systemctl start docker` (Linux). Check container logs in `groups/main/logs/container-*.log`.
@@ -218,6 +285,6 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/
**Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw`
## Diagnostics
## 9. Diagnostics
Read and follow [diagnostics.md](diagnostics.md).
Send diagnostics data by following `.claude/skills/setup/diagnostics.md`.

View File

@@ -42,5 +42,5 @@ rm /tmp/nanoclaw-diagnostics.json
**Never ask again**:
1. Replace contents of `.claude/skills/setup/diagnostics.md` with `# Diagnostics — opted out`
2. Replace contents of `.claude/skills/update-nanoclaw/diagnostics.md` with `# Diagnostics — opted out`
3. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md`
3. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics` section from `.claude/skills/update-nanoclaw/SKILL.md`
4. `rm /tmp/nanoclaw-diagnostics.json`

View File

@@ -237,4 +237,4 @@ Tell the user:
## Diagnostics
Read and follow [diagnostics.md](diagnostics.md).
Send diagnostics data by following `.claude/skills/update-nanoclaw/diagnostics.md`.

View File

@@ -45,5 +45,5 @@ rm /tmp/nanoclaw-diagnostics.json
**Never ask again**:
1. Replace contents of `.claude/skills/setup/diagnostics.md` with `# Diagnostics — opted out`
2. Replace contents of `.claude/skills/update-nanoclaw/diagnostics.md` with `# Diagnostics — opted out`
3. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md`
3. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics` section from `.claude/skills/update-nanoclaw/SKILL.md`
4. `rm /tmp/nanoclaw-diagnostics.json`

View File

@@ -0,0 +1,157 @@
---
name: use-native-credential-proxy
description: Replace OneCLI gateway with the built-in credential proxy. For users who want simple .env-based credential management without installing OneCLI. Reads API key or OAuth token from .env and injects into container API requests.
---
# Use Native Credential Proxy
This skill replaces the OneCLI gateway with NanoClaw's built-in credential proxy. Containers get credentials injected via a local HTTP proxy that reads from `.env` — no external services needed.
## Phase 1: Pre-flight
### Check if already applied
Check if `src/credential-proxy.ts` is imported in `src/index.ts`:
```bash
grep "credential-proxy" src/index.ts
```
If it shows an import for `startCredentialProxy`, the native proxy is already active. Skip to Phase 3 (Setup).
### Check if OneCLI is active
```bash
grep "@onecli-sh/sdk" package.json
```
If `@onecli-sh/sdk` appears, OneCLI is the active credential provider. Proceed with Phase 2 to replace it.
If neither check matches, you may be on an older version. Run `/update-nanoclaw` first, then retry.
## Phase 2: Apply Code Changes
### Ensure upstream remote
```bash
git remote -v
```
If `upstream` is missing, add it:
```bash
git remote add upstream https://github.com/qwibitai/nanoclaw.git
```
### Merge the skill branch
```bash
git fetch upstream skill/native-credential-proxy
git merge upstream/skill/native-credential-proxy || {
git checkout --theirs package-lock.json
git add package-lock.json
git merge --continue
}
```
This merges in:
- `src/credential-proxy.ts` and `src/credential-proxy.test.ts` (the proxy implementation)
- Restored credential proxy usage in `src/index.ts`, `src/container-runner.ts`, `src/container-runtime.ts`, `src/config.ts`
- Removed `@onecli-sh/sdk` dependency
- Restored `CREDENTIAL_PROXY_PORT` config (default 3001)
- Restored platform-aware proxy bind address detection
- Reverted setup skill to `.env`-based credential instructions
If the merge reports conflicts beyond `package-lock.json`, resolve them by reading the conflicted files and understanding the intent of both sides.
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/credential-proxy.test.ts src/container-runner.test.ts
```
All tests must pass and build must be clean before proceeding.
## Phase 3: Setup Credentials
AskUserQuestion: Do you want to use your **Claude subscription** (Pro/Max) or an **Anthropic API key**?
1. **Claude subscription (Pro/Max)** — description: "Uses your existing Claude Pro or Max subscription. You'll run `claude setup-token` in another terminal to get your token."
2. **Anthropic API key** — description: "Pay-per-use API key from console.anthropic.com."
### Subscription path
Tell the user to run `claude setup-token` in another terminal and copy the token it outputs. Do NOT collect the token in chat.
Once they have the token, add it to `.env`:
```bash
# Add to .env (create file if needed)
echo 'CLAUDE_CODE_OAUTH_TOKEN=<token>' >> .env
```
Note: `ANTHROPIC_AUTH_TOKEN` is also supported as a fallback.
### API key path
Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one.
Add it to `.env`:
```bash
echo 'ANTHROPIC_API_KEY=<key>' >> .env
```
### After either path
**If the user's response happens to contain a token or key** (starts with `sk-ant-` or looks like a token): write it to `.env` on their behalf using the appropriate variable name.
**Optional:** If the user needs a custom API endpoint, they can add `ANTHROPIC_BASE_URL=<url>` to `.env` (defaults to `https://api.anthropic.com`).
## Phase 4: Verify
1. Rebuild and restart:
```bash
npm run build
```
Then restart the service:
- macOS: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
- Linux: `systemctl --user restart nanoclaw`
- WSL/manual: stop and re-run `bash start-nanoclaw.sh`
2. Check logs for successful proxy startup:
```bash
tail -20 logs/nanoclaw.log | grep "Credential proxy"
```
Expected: `Credential proxy started` with port and auth mode.
3. Send a test message in the registered chat to verify the agent responds.
4. Note: after applying this skill, the OneCLI credential steps in `/setup` no longer apply. `.env` is now the credential source.
## Troubleshooting
**"Credential proxy upstream error" in logs:** Check that `.env` has a valid `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`. Verify the API is reachable: `curl -s https://api.anthropic.com/v1/messages -H "x-api-key: test" | head`.
**Port 3001 already in use:** Set `CREDENTIAL_PROXY_PORT=<other port>` in `.env` or as an environment variable.
**Container can't reach proxy (Linux):** The proxy binds to the `docker0` bridge IP by default. If that interface doesn't exist (e.g. rootless Docker), set `CREDENTIAL_PROXY_HOST=0.0.0.0` as an environment variable.
**OAuth token expired (401 errors):** Re-run `claude setup-token` in a terminal and update the token in `.env`.
## Removal
To revert to OneCLI gateway:
1. Find the merge commit: `git log --oneline --merges -5`
2. Revert it: `git revert <merge-commit> -m 1` (undoes the skill branch merge, keeps your other changes)
3. `npm install` (re-adds `@onecli-sh/sdk`)
4. `npm run build`
5. Follow `/setup` step 4 to configure OneCLI credentials
6. Remove `ANTHROPIC_API_KEY` / `CLAUDE_CODE_OAUTH_TOKEN` from `.env`

View File

@@ -1 +1 @@
TELEGRAM_BOT_TOKEN=

View File

@@ -1,160 +0,0 @@
name: Merge-forward skill branches
on:
push:
branches: [main]
permissions:
contents: write
issues: write
jobs:
merge-forward:
if: github.repository == 'qwibitai/nanoclaw'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Merge main into each skill branch
id: merge
run: |
FAILED=""
SUCCEEDED=""
# List all remote skill branches
SKILL_BRANCHES=$(git branch -r --list 'origin/skill/*' | sed 's|origin/||' | xargs)
if [ -z "$SKILL_BRANCHES" ]; then
echo "No skill branches found."
exit 0
fi
for BRANCH in $SKILL_BRANCHES; do
SKILL_NAME=$(echo "$BRANCH" | sed 's|skill/||')
echo ""
echo "=== Processing $BRANCH ==="
# Checkout the skill branch
git checkout -B "$BRANCH" "origin/$BRANCH"
# Attempt merge
if ! git merge main --no-edit; then
echo "::warning::Merge conflict in $BRANCH"
git merge --abort
FAILED="$FAILED $SKILL_NAME"
continue
fi
# Check if there's anything new to push
if git diff --quiet "origin/$BRANCH"; then
echo "$BRANCH is already up to date with main."
SUCCEEDED="$SUCCEEDED $SKILL_NAME"
continue
fi
# Install deps and validate
npm ci
if ! npm run build; then
echo "::warning::Build failed for $BRANCH"
git reset --hard "origin/$BRANCH"
FAILED="$FAILED $SKILL_NAME"
continue
fi
if ! npm test 2>/dev/null; then
echo "::warning::Tests failed for $BRANCH"
git reset --hard "origin/$BRANCH"
FAILED="$FAILED $SKILL_NAME"
continue
fi
# Push the updated branch
git push origin "$BRANCH"
SUCCEEDED="$SUCCEEDED $SKILL_NAME"
echo "$BRANCH merged and pushed successfully."
done
echo ""
echo "=== Results ==="
echo "Succeeded: $SUCCEEDED"
echo "Failed: $FAILED"
# Export for issue creation
echo "failed=$FAILED" >> "$GITHUB_OUTPUT"
echo "succeeded=$SUCCEEDED" >> "$GITHUB_OUTPUT"
- name: Open issue for failed merges
if: steps.merge.outputs.failed != ''
uses: actions/github-script@v7
with:
script: |
const failed = '${{ steps.merge.outputs.failed }}'.trim().split(/\s+/);
const sha = context.sha.substring(0, 7);
const body = [
`The merge-forward workflow failed to merge \`main\` (${sha}) into the following skill branches:`,
'',
...failed.map(s => `- \`skill/${s}\`: merge conflict, build failure, or test failure`),
'',
'Please resolve manually:',
'```bash',
...failed.map(s => [
`git checkout skill/${s}`,
`git merge main`,
`# resolve conflicts, then: git push`,
''
]).flat(),
'```',
'',
`Triggered by push to main: ${context.sha}`
].join('\n');
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Merge-forward failed for ${failed.length} skill branch(es) after ${sha}`,
body,
labels: ['skill-maintenance']
});
- name: Notify channel forks
if: always()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.FORK_DISPATCH_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const forks = [
'nanoclaw-whatsapp',
'nanoclaw-telegram',
'nanoclaw-discord',
'nanoclaw-slack',
'nanoclaw-gmail',
'nanoclaw-docker-sandboxes',
];
const sha = context.sha.substring(0, 7);
for (const repo of forks) {
try {
await github.rest.repos.createDispatchEvent({
owner: 'qwibitai',
repo,
event_type: 'upstream-main-updated',
client_payload: { sha: context.sha },
});
console.log(`Notified ${repo}`);
} catch (e) {
console.log(`Failed to notify ${repo}: ${e.message}`);
}
}

View File

@@ -21,6 +21,10 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele
| `groups/{name}/CLAUDE.md` | Per-group memory (isolated) |
| `container/skills/` | Skills loaded inside agent containers (browser, status, formatting) |
## Secrets / Credentials / Proxy (OneCLI)
API keys, secret keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway — which handles secret injection into containers at request time, so no keys or tokens are ever passed to containers directly. Run `onecli --help`.
## Skills
Four types of skills exist in NanoClaw. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy and guidelines.

276
package-lock.json generated
View File

@@ -1,15 +1,16 @@
{
"name": "nanoclaw",
"version": "1.2.21",
"version": "1.2.27",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nanoclaw",
"version": "1.2.21",
"version": "1.2.27",
"dependencies": {
"better-sqlite3": "^11.8.1",
"cron-parser": "^5.5.0",
"@onecli-sh/sdk": "^0.2.0",
"better-sqlite3": "11.10.0",
"cron-parser": "5.5.0",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"yaml": "^2.8.2",
@@ -541,7 +542,6 @@
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
},
@@ -560,7 +560,6 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -573,7 +572,6 @@
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
@@ -583,7 +581,6 @@
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
"integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
@@ -598,7 +595,6 @@
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0"
},
@@ -611,7 +607,6 @@
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
@@ -624,7 +619,6 @@
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
"integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.14.0",
"debug": "^4.3.2",
@@ -648,7 +642,6 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
@@ -661,7 +654,6 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
@@ -674,7 +666,6 @@
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
"integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -687,7 +678,6 @@
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
@@ -697,7 +687,6 @@
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0",
"levn": "^0.4.1"
@@ -711,7 +700,6 @@
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
@@ -721,7 +709,6 @@
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.4.0"
@@ -735,7 +722,6 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
},
@@ -749,7 +735,6 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
@@ -786,6 +771,14 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@onecli-sh/sdk": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@onecli-sh/sdk/-/sdk-0.2.0.tgz",
"integrity": "sha512-u7PqWROEvTV9f0ADVkjigTrd2AZn3klbPrv7GGpeRHIJpjAxJUdlWqxr5kiGt6qTDKL8t3nq76xr4X2pxTiyBg==",
"engines": {
"node": ">=20"
}
},
"node_modules/@pinojs/redact": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
@@ -1188,8 +1181,7 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/@types/node": {
"version": "22.19.11",
@@ -1202,17 +1194,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
"integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
"integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/type-utils": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"@typescript-eslint/scope-manager": "8.57.2",
"@typescript-eslint/type-utils": "8.57.2",
"@typescript-eslint/utils": "8.57.2",
"@typescript-eslint/visitor-keys": "8.57.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
@@ -1225,7 +1216,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.57.1",
"@typescript-eslint/parser": "^8.57.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -1235,22 +1226,20 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz",
"integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz",
"integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"@typescript-eslint/scope-manager": "8.57.2",
"@typescript-eslint/types": "8.57.2",
"@typescript-eslint/typescript-estree": "8.57.2",
"@typescript-eslint/visitor-keys": "8.57.2",
"debug": "^4.4.3"
},
"engines": {
@@ -1266,14 +1255,13 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz",
"integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz",
"integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.57.1",
"@typescript-eslint/types": "^8.57.1",
"@typescript-eslint/tsconfig-utils": "^8.57.2",
"@typescript-eslint/types": "^8.57.2",
"debug": "^4.4.3"
},
"engines": {
@@ -1288,14 +1276,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz",
"integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz",
"integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1"
"@typescript-eslint/types": "8.57.2",
"@typescript-eslint/visitor-keys": "8.57.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1306,11 +1293,10 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz",
"integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz",
"integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -1323,15 +1309,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz",
"integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz",
"integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"@typescript-eslint/types": "8.57.2",
"@typescript-eslint/typescript-estree": "8.57.2",
"@typescript-eslint/utils": "8.57.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
},
@@ -1348,11 +1333,10 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz",
"integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz",
"integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -1362,16 +1346,15 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz",
"integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz",
"integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.57.1",
"@typescript-eslint/tsconfig-utils": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"@typescript-eslint/project-service": "8.57.2",
"@typescript-eslint/tsconfig-utils": "8.57.2",
"@typescript-eslint/types": "8.57.2",
"@typescript-eslint/visitor-keys": "8.57.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1394,17 +1377,15 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
@@ -1417,7 +1398,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
},
@@ -1429,16 +1409,15 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz",
"integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz",
"integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1"
"@typescript-eslint/scope-manager": "8.57.2",
"@typescript-eslint/types": "8.57.2",
"@typescript-eslint/typescript-estree": "8.57.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1453,13 +1432,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz",
"integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz",
"integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/types": "8.57.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1475,7 +1453,6 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
@@ -1630,7 +1607,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -1643,7 +1619,6 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -1653,7 +1628,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -1670,7 +1644,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -1685,8 +1658,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
"dev": true
},
"node_modules/assertion-error": {
"version": "2.0.1",
@@ -1723,8 +1695,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -1782,7 +1753,6 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -1817,7 +1787,6 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -1837,7 +1806,6 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -1860,7 +1828,6 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1872,8 +1839,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/colorette": {
"version": "2.0.20",
@@ -1885,8 +1851,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/cron-parser": {
"version": "5.5.0",
@@ -1905,7 +1870,6 @@
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -1970,8 +1934,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/detect-libc": {
"version": "2.1.2",
@@ -2045,7 +2008,6 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
@@ -2058,7 +2020,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2118,7 +2079,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-no-catch-all/-/eslint-plugin-no-catch-all-1.1.0.tgz",
"integrity": "sha512-VkP62jLTmccPrFGN/W6V7a3SEwdtTZm+Su2k4T3uyJirtkm0OMMm97h7qd8pRFAHus/jQg9FpUpLRc7sAylBEQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"eslint": ">=2.0.0"
}
@@ -2128,7 +2088,6 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@@ -2145,7 +2104,6 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -2158,7 +2116,6 @@
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
@@ -2176,7 +2133,6 @@
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -2189,7 +2145,6 @@
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -2202,7 +2157,6 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -2222,7 +2176,6 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -2256,22 +2209,19 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
@@ -2302,7 +2252,6 @@
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^4.0.0"
},
@@ -2321,7 +2270,6 @@
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@@ -2338,7 +2286,6 @@
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.4"
@@ -2351,8 +2298,7 @@
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
"dev": true
},
"node_modules/fs-constants": {
"version": "1.0.0",
@@ -2399,7 +2345,6 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -2412,7 +2357,6 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
@@ -2484,7 +2428,6 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
@@ -2494,7 +2437,6 @@
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -2511,7 +2453,6 @@
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
}
@@ -2533,7 +2474,6 @@
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -2543,7 +2483,6 @@
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -2555,8 +2494,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
"dev": true
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
@@ -2618,7 +2556,6 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -2630,29 +2567,25 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
@@ -2662,7 +2595,6 @@
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -2676,7 +2608,6 @@
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
},
@@ -2691,8 +2622,7 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/luxon": {
"version": "3.7.2",
@@ -2758,7 +2688,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -2817,8 +2746,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/node-abi": {
"version": "3.87.0",
@@ -2866,7 +2794,6 @@
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -2884,7 +2811,6 @@
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -2900,7 +2826,6 @@
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
},
@@ -2916,7 +2841,6 @@
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
@@ -2929,7 +2853,6 @@
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -2939,7 +2862,6 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -3101,7 +3023,6 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
@@ -3153,7 +3074,6 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -3216,7 +3136,6 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
@@ -3338,7 +3257,6 @@
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -3351,7 +3269,6 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -3570,7 +3487,6 @@
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
@@ -3615,7 +3531,6 @@
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
@@ -3638,16 +3553,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz",
"integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==",
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz",
"integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1",
"@typescript-eslint/utils": "8.57.1"
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@typescript-eslint/typescript-estree": "8.57.2",
"@typescript-eslint/utils": "8.57.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3673,7 +3587,6 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
@@ -3842,7 +3755,6 @@
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
@@ -3875,7 +3787,6 @@
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -3906,7 +3817,6 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},

View File

@@ -1,6 +1,6 @@
{
"name": "nanoclaw",
"version": "1.2.21",
"version": "1.2.27",
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
"type": "module",
"main": "dist/index.js",
@@ -21,8 +21,9 @@
"test:watch": "vitest"
},
"dependencies": {
"better-sqlite3": "^11.8.1",
"cron-parser": "^5.5.0",
"@onecli-sh/sdk": "^0.2.0",
"better-sqlite3": "11.10.0",
"cron-parser": "5.5.0",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"yaml": "^2.8.2",

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="97" height="20" role="img" aria-label="40.9k tokens, 20% of context window">
<title>40.9k tokens, 20% of context window</title>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="97" height="20" role="img" aria-label="40.1k tokens, 20% of context window">
<title>40.1k tokens, 20% of context window</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@@ -15,8 +15,8 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
<text aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
<text x="26" y="14">tokens</text>
<text aria-hidden="true" x="74" y="15" fill="#010101" fill-opacity=".3">40.9k</text>
<text x="74" y="14">40.9k</text>
<text aria-hidden="true" x="74" y="15" fill="#010101" fill-opacity=".3">40.1k</text>
<text x="74" y="14">40.1k</text>
</g>
</g>
</a>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -9,6 +9,7 @@ const STEPS: Record<
string,
() => Promise<{ run: (args: string[]) => Promise<void> }>
> = {
timezone: () => import('./timezone.js'),
environment: () => import('./environment.js'),
container: () => import('./container.js'),
groups: () => import('./groups.js'),

67
setup/timezone.ts Normal file
View File

@@ -0,0 +1,67 @@
/**
* Step: timezone — Detect, validate, and persist the user's timezone.
* Writes TZ to .env if a valid IANA timezone is resolved.
* Emits NEEDS_USER_INPUT=true when autodetection fails.
*/
import fs from 'fs';
import path from 'path';
import { isValidTimezone } from '../src/timezone.js';
import { logger } from '../src/logger.js';
import { emitStatus } from './status.js';
export async function run(args: string[]): Promise<void> {
const projectRoot = process.cwd();
const envFile = path.join(projectRoot, '.env');
// Check what's already in .env
let envFileTz: string | undefined;
if (fs.existsSync(envFile)) {
const content = fs.readFileSync(envFile, 'utf-8');
const match = content.match(/^TZ=(.+)$/m);
if (match) envFileTz = match[1].trim().replace(/^["']|["']$/g, '');
}
const systemTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const envTz = process.env.TZ;
// Accept --tz flag from CLI (used when setup skill collects from user)
const tzFlagIdx = args.indexOf('--tz');
const userTz = tzFlagIdx !== -1 ? args[tzFlagIdx + 1] : undefined;
// Resolve: user-provided > .env > process.env > system autodetect
let resolvedTz: string | undefined;
for (const candidate of [userTz, envFileTz, envTz, systemTz]) {
if (candidate && isValidTimezone(candidate)) {
resolvedTz = candidate;
break;
}
}
const needsUserInput = !resolvedTz;
if (resolvedTz && resolvedTz !== envFileTz) {
// Write/update TZ in .env
if (fs.existsSync(envFile)) {
let content = fs.readFileSync(envFile, 'utf-8');
if (/^TZ=/m.test(content)) {
content = content.replace(/^TZ=.*$/m, `TZ=${resolvedTz}`);
} else {
content = content.trimEnd() + `\nTZ=${resolvedTz}\n`;
}
fs.writeFileSync(envFile, content);
} else {
fs.writeFileSync(envFile, `TZ=${resolvedTz}\n`);
}
logger.info({ timezone: resolvedTz }, 'Set TZ in .env');
}
emitStatus('TIMEZONE', {
SYSTEM_TZ: systemTz || 'unknown',
ENV_TZ: envTz || 'unset',
ENV_FILE_TZ: envFileTz || 'unset',
RESOLVED_TZ: resolvedTz || 'none',
NEEDS_USER_INPUT: needsUserInput,
STATUS: needsUserInput ? 'needs_input' : 'success',
});
}

View File

@@ -101,7 +101,7 @@ export async function run(_args: string[]): Promise<void> {
const envFile = path.join(projectRoot, '.env');
if (fs.existsSync(envFile)) {
const envContent = fs.readFileSync(envFile, 'utf-8');
if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(envContent)) {
if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ONECLI_URL)=/m.test(envContent)) {
credentials = 'configured';
}
}

View File

@@ -2,11 +2,15 @@ import os from 'os';
import path from 'path';
import { readEnvFile } from './env.js';
import { isValidTimezone } from './timezone.js';
// Read config values from .env (falls back to process.env).
// Secrets (API keys, tokens) are NOT read here — they are loaded only
// by the credential proxy (credential-proxy.ts), never exposed to containers.
const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER']);
const envConfig = readEnvFile([
'ASSISTANT_NAME',
'ASSISTANT_HAS_OWN_NUMBER',
'ONECLI_URL',
'TZ',
]);
export const ASSISTANT_NAME =
process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy';
@@ -47,10 +51,8 @@ export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(
process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760',
10,
); // 10MB default
export const CREDENTIAL_PROXY_PORT = parseInt(
process.env.CREDENTIAL_PROXY_PORT || '3001',
10,
);
export const ONECLI_URL =
process.env.ONECLI_URL || envConfig.ONECLI_URL || 'http://localhost:10254';
export const IPC_POLL_INTERVAL = 1000;
export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep container alive after last result
export const MAX_CONCURRENT_CONTAINERS = Math.max(
@@ -67,7 +69,17 @@ export const TRIGGER_PATTERN = new RegExp(
'i',
);
// Timezone for scheduled tasks (cron expressions, etc.)
// Uses system timezone by default
export const TIMEZONE =
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
// Timezone for scheduled tasks, message formatting, etc.
// Validates each candidate is a real IANA identifier before accepting.
function resolveConfigTimezone(): string {
const candidates = [
process.env.TZ,
envConfig.TZ,
Intl.DateTimeFormat().resolvedOptions().timeZone,
];
for (const tz of candidates) {
if (tz && isValidTimezone(tz)) return tz;
}
return 'UTC';
}
export const TIMEZONE = resolveConfigTimezone();

View File

@@ -11,10 +11,10 @@ vi.mock('./config.js', () => ({
CONTAINER_IMAGE: 'nanoclaw-agent:latest',
CONTAINER_MAX_OUTPUT_SIZE: 10485760,
CONTAINER_TIMEOUT: 1800000, // 30min
CREDENTIAL_PROXY_PORT: 3001,
DATA_DIR: '/tmp/nanoclaw-test-data',
GROUPS_DIR: '/tmp/nanoclaw-test-groups',
IDLE_TIMEOUT: 1800000, // 30min
ONECLI_URL: 'http://localhost:10254',
TIMEZONE: 'America/Los_Angeles',
}));
@@ -51,6 +51,17 @@ vi.mock('./mount-security.js', () => ({
validateAdditionalMounts: vi.fn(() => []),
}));
// Mock OneCLI SDK
vi.mock('@onecli-sh/sdk', () => ({
OneCLI: class {
applyContainerConfig = vi.fn().mockResolvedValue(true);
createAgent = vi.fn().mockResolvedValue({ id: 'test' });
ensureAgent = vi
.fn()
.mockResolvedValue({ name: 'test', identifier: 'test', created: true });
},
}));
// Create a controllable fake ChildProcess
function createFakeProcess() {
const proc = new EventEmitter() as EventEmitter & {

View File

@@ -10,25 +10,26 @@ import {
CONTAINER_IMAGE,
CONTAINER_MAX_OUTPUT_SIZE,
CONTAINER_TIMEOUT,
CREDENTIAL_PROXY_PORT,
DATA_DIR,
GROUPS_DIR,
IDLE_TIMEOUT,
ONECLI_URL,
TIMEZONE,
} from './config.js';
import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js';
import { logger } from './logger.js';
import {
CONTAINER_HOST_GATEWAY,
CONTAINER_RUNTIME_BIN,
hostGatewayArgs,
readonlyMountArgs,
stopContainer,
} from './container-runtime.js';
import { detectAuthMode } from './credential-proxy.js';
import { OneCLI } from '@onecli-sh/sdk';
import { validateAdditionalMounts } from './mount-security.js';
import { RegisteredGroup } from './types.js';
const onecli = new OneCLI({ url: ONECLI_URL });
// Sentinel markers for robust output parsing (must match agent-runner)
const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
@@ -77,7 +78,7 @@ function buildVolumeMounts(
});
// Shadow .env so the agent cannot read secrets from the mounted project root.
// Credentials are injected by the credential proxy, never exposed to containers.
// Credentials are injected by the OneCLI gateway, never exposed to containers.
const envFile = path.join(projectRoot, '.env');
if (fs.existsSync(envFile)) {
mounts.push({
@@ -212,30 +213,29 @@ function buildVolumeMounts(
return mounts;
}
function buildContainerArgs(
async function buildContainerArgs(
mounts: VolumeMount[],
containerName: string,
): string[] {
agentIdentifier?: string,
): Promise<string[]> {
const args: string[] = ['run', '-i', '--rm', '--name', containerName];
// Pass host timezone so container's local time matches the user's
args.push('-e', `TZ=${TIMEZONE}`);
// Route API traffic through the credential proxy (containers never see real secrets)
args.push(
'-e',
`ANTHROPIC_BASE_URL=http://${CONTAINER_HOST_GATEWAY}:${CREDENTIAL_PROXY_PORT}`,
);
// Mirror the host's auth method with a placeholder value.
// API key mode: SDK sends x-api-key, proxy replaces with real key.
// OAuth mode: SDK exchanges placeholder token for temp API key,
// proxy injects real OAuth token on that exchange request.
const authMode = detectAuthMode();
if (authMode === 'api-key') {
args.push('-e', 'ANTHROPIC_API_KEY=placeholder');
// OneCLI gateway handles credential injection — containers never see real secrets.
// The gateway intercepts HTTPS traffic and injects API keys or OAuth tokens.
const onecliApplied = await onecli.applyContainerConfig(args, {
addHostMapping: false, // Nanoclaw already handles host gateway
agent: agentIdentifier,
});
if (onecliApplied) {
logger.info({ containerName }, 'OneCLI gateway config applied');
} else {
args.push('-e', 'CLAUDE_CODE_OAUTH_TOKEN=placeholder');
logger.warn(
{ containerName },
'OneCLI gateway not reachable — container will have no credentials',
);
}
// Runtime-specific args for host gateway resolution
@@ -278,7 +278,15 @@ export async function runContainerAgent(
const mounts = buildVolumeMounts(group, input.isMain);
const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-');
const containerName = `nanoclaw-${safeName}-${Date.now()}`;
const containerArgs = buildContainerArgs(mounts, containerName);
// Main group uses the default OneCLI agent; others use their own agent.
const agentIdentifier = input.isMain
? undefined
: group.folder.toLowerCase().replace(/_/g, '-');
const containerArgs = await buildContainerArgs(
mounts,
containerName,
agentIdentifier,
);
logger.debug(
{

View File

@@ -3,7 +3,6 @@
* All runtime-specific logic lives here so swapping runtimes means changing one file.
*/
import { execSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import { logger } from './logger.js';
@@ -11,35 +10,6 @@ import { logger } from './logger.js';
/** The container runtime binary name. */
export const CONTAINER_RUNTIME_BIN = 'docker';
/** Hostname containers use to reach the host machine. */
export const CONTAINER_HOST_GATEWAY = 'host.docker.internal';
/**
* Address the credential proxy binds to.
* Docker Desktop (macOS): 127.0.0.1 — the VM routes host.docker.internal to loopback.
* Docker (Linux): bind to the docker0 bridge IP so only containers can reach it,
* falling back to 0.0.0.0 if the interface isn't found.
*/
export const PROXY_BIND_HOST =
process.env.CREDENTIAL_PROXY_HOST || detectProxyBindHost();
function detectProxyBindHost(): string {
if (os.platform() === 'darwin') return '127.0.0.1';
// WSL uses Docker Desktop (same VM routing as macOS) — loopback is correct.
// Check /proc filesystem, not env vars — WSL_DISTRO_NAME isn't set under systemd.
if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop')) return '127.0.0.1';
// Bare-metal Linux: bind to the docker0 bridge IP instead of 0.0.0.0
const ifaces = os.networkInterfaces();
const docker0 = ifaces['docker0'];
if (docker0) {
const ipv4 = docker0.find((a) => a.family === 'IPv4');
if (ipv4) return ipv4.address;
}
return '0.0.0.0';
}
/** CLI args needed for the container to resolve the host gateway. */
export function hostGatewayArgs(): string[] {
// On Linux, host.docker.internal isn't built-in — add it explicitly

View File

@@ -1,192 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import http from 'http';
import type { AddressInfo } from 'net';
const mockEnv: Record<string, string> = {};
vi.mock('./env.js', () => ({
readEnvFile: vi.fn(() => ({ ...mockEnv })),
}));
vi.mock('./logger.js', () => ({
logger: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() },
}));
import { startCredentialProxy } from './credential-proxy.js';
function makeRequest(
port: number,
options: http.RequestOptions,
body = '',
): Promise<{
statusCode: number;
body: string;
headers: http.IncomingHttpHeaders;
}> {
return new Promise((resolve, reject) => {
const req = http.request(
{ ...options, hostname: '127.0.0.1', port },
(res) => {
const chunks: Buffer[] = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
resolve({
statusCode: res.statusCode!,
body: Buffer.concat(chunks).toString(),
headers: res.headers,
});
});
},
);
req.on('error', reject);
req.write(body);
req.end();
});
}
describe('credential-proxy', () => {
let proxyServer: http.Server;
let upstreamServer: http.Server;
let proxyPort: number;
let upstreamPort: number;
let lastUpstreamHeaders: http.IncomingHttpHeaders;
beforeEach(async () => {
lastUpstreamHeaders = {};
upstreamServer = http.createServer((req, res) => {
lastUpstreamHeaders = { ...req.headers };
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
});
await new Promise<void>((resolve) =>
upstreamServer.listen(0, '127.0.0.1', resolve),
);
upstreamPort = (upstreamServer.address() as AddressInfo).port;
});
afterEach(async () => {
await new Promise<void>((r) => proxyServer?.close(() => r()));
await new Promise<void>((r) => upstreamServer?.close(() => r()));
for (const key of Object.keys(mockEnv)) delete mockEnv[key];
});
async function startProxy(env: Record<string, string>): Promise<number> {
Object.assign(mockEnv, env, {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${upstreamPort}`,
});
proxyServer = await startCredentialProxy(0);
return (proxyServer.address() as AddressInfo).port;
}
it('API-key mode injects x-api-key and strips placeholder', async () => {
proxyPort = await startProxy({ ANTHROPIC_API_KEY: 'sk-ant-real-key' });
await makeRequest(
proxyPort,
{
method: 'POST',
path: '/v1/messages',
headers: {
'content-type': 'application/json',
'x-api-key': 'placeholder',
},
},
'{}',
);
expect(lastUpstreamHeaders['x-api-key']).toBe('sk-ant-real-key');
});
it('OAuth mode replaces Authorization when container sends one', async () => {
proxyPort = await startProxy({
CLAUDE_CODE_OAUTH_TOKEN: 'real-oauth-token',
});
await makeRequest(
proxyPort,
{
method: 'POST',
path: '/api/oauth/claude_cli/create_api_key',
headers: {
'content-type': 'application/json',
authorization: 'Bearer placeholder',
},
},
'{}',
);
expect(lastUpstreamHeaders['authorization']).toBe(
'Bearer real-oauth-token',
);
});
it('OAuth mode does not inject Authorization when container omits it', async () => {
proxyPort = await startProxy({
CLAUDE_CODE_OAUTH_TOKEN: 'real-oauth-token',
});
// Post-exchange: container uses x-api-key only, no Authorization header
await makeRequest(
proxyPort,
{
method: 'POST',
path: '/v1/messages',
headers: {
'content-type': 'application/json',
'x-api-key': 'temp-key-from-exchange',
},
},
'{}',
);
expect(lastUpstreamHeaders['x-api-key']).toBe('temp-key-from-exchange');
expect(lastUpstreamHeaders['authorization']).toBeUndefined();
});
it('strips hop-by-hop headers', async () => {
proxyPort = await startProxy({ ANTHROPIC_API_KEY: 'sk-ant-real-key' });
await makeRequest(
proxyPort,
{
method: 'POST',
path: '/v1/messages',
headers: {
'content-type': 'application/json',
connection: 'keep-alive',
'keep-alive': 'timeout=5',
'transfer-encoding': 'chunked',
},
},
'{}',
);
// Proxy strips client hop-by-hop headers. Node's HTTP client may re-add
// its own Connection header (standard HTTP/1.1 behavior), but the client's
// custom keep-alive and transfer-encoding must not be forwarded.
expect(lastUpstreamHeaders['keep-alive']).toBeUndefined();
expect(lastUpstreamHeaders['transfer-encoding']).toBeUndefined();
});
it('returns 502 when upstream is unreachable', async () => {
Object.assign(mockEnv, {
ANTHROPIC_API_KEY: 'sk-ant-real-key',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:59999',
});
proxyServer = await startCredentialProxy(0);
proxyPort = (proxyServer.address() as AddressInfo).port;
const res = await makeRequest(
proxyPort,
{
method: 'POST',
path: '/v1/messages',
headers: { 'content-type': 'application/json' },
},
'{}',
);
expect(res.statusCode).toBe(502);
expect(res.body).toBe('Bad Gateway');
});
});

View File

@@ -1,125 +0,0 @@
/**
* Credential proxy for container isolation.
* Containers connect here instead of directly to the Anthropic API.
* The proxy injects real credentials so containers never see them.
*
* Two auth modes:
* API key: Proxy injects x-api-key on every request.
* OAuth: Container CLI exchanges its placeholder token for a temp
* API key via /api/oauth/claude_cli/create_api_key.
* Proxy injects real OAuth token on that exchange request;
* subsequent requests carry the temp key which is valid as-is.
*/
import { createServer, Server } from 'http';
import { request as httpsRequest } from 'https';
import { request as httpRequest, RequestOptions } from 'http';
import { readEnvFile } from './env.js';
import { logger } from './logger.js';
export type AuthMode = 'api-key' | 'oauth';
export interface ProxyConfig {
authMode: AuthMode;
}
export function startCredentialProxy(
port: number,
host = '127.0.0.1',
): Promise<Server> {
const secrets = readEnvFile([
'ANTHROPIC_API_KEY',
'CLAUDE_CODE_OAUTH_TOKEN',
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_BASE_URL',
]);
const authMode: AuthMode = secrets.ANTHROPIC_API_KEY ? 'api-key' : 'oauth';
const oauthToken =
secrets.CLAUDE_CODE_OAUTH_TOKEN || secrets.ANTHROPIC_AUTH_TOKEN;
const upstreamUrl = new URL(
secrets.ANTHROPIC_BASE_URL || 'https://api.anthropic.com',
);
const isHttps = upstreamUrl.protocol === 'https:';
const makeRequest = isHttps ? httpsRequest : httpRequest;
return new Promise((resolve, reject) => {
const server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const body = Buffer.concat(chunks);
const headers: Record<string, string | number | string[] | undefined> =
{
...(req.headers as Record<string, string>),
host: upstreamUrl.host,
'content-length': body.length,
};
// Strip hop-by-hop headers that must not be forwarded by proxies
delete headers['connection'];
delete headers['keep-alive'];
delete headers['transfer-encoding'];
if (authMode === 'api-key') {
// API key mode: inject x-api-key on every request
delete headers['x-api-key'];
headers['x-api-key'] = secrets.ANTHROPIC_API_KEY;
} else {
// OAuth mode: replace placeholder Bearer token with the real one
// only when the container actually sends an Authorization header
// (exchange request + auth probes). Post-exchange requests use
// x-api-key only, so they pass through without token injection.
if (headers['authorization']) {
delete headers['authorization'];
if (oauthToken) {
headers['authorization'] = `Bearer ${oauthToken}`;
}
}
}
const upstream = makeRequest(
{
hostname: upstreamUrl.hostname,
port: upstreamUrl.port || (isHttps ? 443 : 80),
path: req.url,
method: req.method,
headers,
} as RequestOptions,
(upRes) => {
res.writeHead(upRes.statusCode!, upRes.headers);
upRes.pipe(res);
},
);
upstream.on('error', (err) => {
logger.error(
{ err, url: req.url },
'Credential proxy upstream error',
);
if (!res.headersSent) {
res.writeHead(502);
res.end('Bad Gateway');
}
});
upstream.write(body);
upstream.end();
});
});
server.listen(port, host, () => {
logger.info({ port, host, authMode }, 'Credential proxy started');
resolve(server);
});
server.on('error', reject);
});
}
/** Detect which auth mode the host is configured for. */
export function detectAuthMode(): AuthMode {
const secrets = readEnvFile(['ANTHROPIC_API_KEY']);
return secrets.ANTHROPIC_API_KEY ? 'api-key' : 'oauth';
}

View File

@@ -1,15 +1,16 @@
import fs from 'fs';
import path from 'path';
import { OneCLI } from '@onecli-sh/sdk';
import {
ASSISTANT_NAME,
CREDENTIAL_PROXY_PORT,
IDLE_TIMEOUT,
ONECLI_URL,
POLL_INTERVAL,
TIMEZONE,
TRIGGER_PATTERN,
} from './config.js';
import { startCredentialProxy } from './credential-proxy.js';
import './channels/index.js';
import {
getChannelFactory,
@@ -24,7 +25,6 @@ import {
import {
cleanupOrphans,
ensureContainerRuntimeRunning,
PROXY_BIND_HOST,
} from './container-runtime.js';
import {
getAllChats,
@@ -72,6 +72,27 @@ let messageLoopRunning = false;
const channels: Channel[] = [];
const queue = new GroupQueue();
const onecli = new OneCLI({ url: ONECLI_URL });
function ensureOneCLIAgent(jid: string, group: RegisteredGroup): void {
if (group.isMain) return;
const identifier = group.folder.toLowerCase().replace(/_/g, '-');
onecli.ensureAgent({ name: group.name, identifier }).then(
(res) => {
logger.info(
{ jid, identifier, created: res.created },
'OneCLI agent ensured',
);
},
(err) => {
logger.debug(
{ jid, identifier, err: String(err) },
'OneCLI agent ensure skipped',
);
},
);
}
function loadState(): void {
lastTimestamp = getRouterState('last_timestamp') || '';
const agentTs = getRouterState('last_agent_timestamp');
@@ -112,6 +133,9 @@ function registerGroup(jid: string, group: RegisteredGroup): void {
// Create group folder
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
// Ensure a corresponding OneCLI agent exists (best-effort, non-blocking)
ensureOneCLIAgent(jid, group);
logger.info(
{ jid, name: group.name, folder: group.folder },
'Group registered',
@@ -474,18 +498,18 @@ async function main(): Promise<void> {
initDatabase();
logger.info('Database initialized');
loadState();
restoreRemoteControl();
// Start credential proxy (containers route API calls through this)
const proxyServer = await startCredentialProxy(
CREDENTIAL_PROXY_PORT,
PROXY_BIND_HOST,
);
// Ensure OneCLI agents exist for all registered groups.
// Recovers from missed creates (e.g. OneCLI was down at registration time).
for (const [jid, group] of Object.entries(registeredGroups)) {
ensureOneCLIAgent(jid, group);
}
restoreRemoteControl();
// Graceful shutdown handlers
const shutdown = async (signal: string) => {
logger.info({ signal }, 'Shutdown signal received');
proxyServer.close();
await queue.shutdown(10000);
for (const ch of channels) await ch.disconnect();
process.exit(0);

View File

@@ -1,6 +1,10 @@
import { describe, it, expect } from 'vitest';
import { formatLocalTime } from './timezone.js';
import {
formatLocalTime,
isValidTimezone,
resolveTimezone,
} from './timezone.js';
// --- formatLocalTime ---
@@ -26,4 +30,44 @@ describe('formatLocalTime', () => {
expect(ny).toContain('8:00');
expect(tokyo).toContain('9:00');
});
it('does not throw on invalid timezone, falls back to UTC', () => {
expect(() =>
formatLocalTime('2026-01-01T00:00:00.000Z', 'IST-2'),
).not.toThrow();
const result = formatLocalTime('2026-01-01T12:00:00.000Z', 'IST-2');
// Should format as UTC (noon UTC = 12:00 PM)
expect(result).toContain('12:00');
expect(result).toContain('PM');
});
});
describe('isValidTimezone', () => {
it('accepts valid IANA identifiers', () => {
expect(isValidTimezone('America/New_York')).toBe(true);
expect(isValidTimezone('UTC')).toBe(true);
expect(isValidTimezone('Asia/Tokyo')).toBe(true);
expect(isValidTimezone('Asia/Jerusalem')).toBe(true);
});
it('rejects invalid timezone strings', () => {
expect(isValidTimezone('IST-2')).toBe(false);
expect(isValidTimezone('XYZ+3')).toBe(false);
});
it('rejects empty and garbage strings', () => {
expect(isValidTimezone('')).toBe(false);
expect(isValidTimezone('NotATimezone')).toBe(false);
});
});
describe('resolveTimezone', () => {
it('returns the timezone if valid', () => {
expect(resolveTimezone('America/New_York')).toBe('America/New_York');
});
it('falls back to UTC for invalid timezone', () => {
expect(resolveTimezone('IST-2')).toBe('UTC');
expect(resolveTimezone('')).toBe('UTC');
});
});

View File

@@ -1,11 +1,32 @@
/**
* Check whether a timezone string is a valid IANA identifier
* that Intl.DateTimeFormat can use.
*/
export function isValidTimezone(tz: string): boolean {
try {
Intl.DateTimeFormat(undefined, { timeZone: tz });
return true;
} catch {
return false;
}
}
/**
* Return the given timezone if valid IANA, otherwise fall back to UTC.
*/
export function resolveTimezone(tz: string): string {
return isValidTimezone(tz) ? tz : 'UTC';
}
/**
* Convert a UTC ISO timestamp to a localized display string.
* Uses the Intl API (no external dependencies).
* Falls back to UTC if the timezone is invalid.
*/
export function formatLocalTime(utcIso: string, timezone: string): string {
const date = new Date(utcIso);
return date.toLocaleString('en-US', {
timeZone: timezone,
timeZone: resolveTimezone(timezone),
year: 'numeric',
month: 'short',
day: 'numeric',