[codex] follow-up: valid pio env CI + dual-hw loop hardening #9
@@ -35,11 +35,15 @@ jobs:
|
||||
run: |
|
||||
shellcheck tools/ai/zeroclaw_dual_bootstrap.sh
|
||||
shellcheck tools/ai/zeroclaw_dual_chat.sh
|
||||
shellcheck tools/ai/zeroclaw_hw_firmware_loop.sh
|
||||
shellcheck tools/ai/zeroclaw_stack_up.sh
|
||||
shellcheck tools/ai/zeroclaw_stack_down.sh
|
||||
shellcheck tools/ai/zeroclaw_watch_1min.sh
|
||||
shellcheck tools/ai/zeroclaw_webhook_send.sh
|
||||
|
||||
- name: Verify spec files
|
||||
run: |
|
||||
test -s specs/03_plan.md
|
||||
test -s specs/04_tasks.md
|
||||
test -s specs/zeroclaw_dual_hw_orchestration_spec.md
|
||||
test -s specs/zeroclaw_dual_hw_todo.md
|
||||
|
||||
@@ -36,7 +36,7 @@ Then commit.
|
||||
## Verify required check contexts
|
||||
|
||||
After your first PR, open **PR → Checks** and note the exact names, e.g.
|
||||
- `Firmware CI / pio (native)`
|
||||
- `Firmware CI / pio`
|
||||
- `Scope Guard / guard`
|
||||
|
||||
Use those exact names in branch protection (the provided `setup_repo.sh` uses the defaults from the template).
|
||||
|
||||
@@ -9,10 +9,6 @@ on:
|
||||
jobs:
|
||||
pio:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
env: [esp32s3_arduino, esp32_arduino, native]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -32,7 +28,7 @@ jobs:
|
||||
- name: No relevant changes (fast pass)
|
||||
if: steps.changes.outputs.fw != 'true'
|
||||
run: |
|
||||
echo "No firmware/spec/standards changes detected. Skipping build/test for ${{ matrix.env }}."
|
||||
echo "No firmware/spec/standards changes detected. Skipping build/test."
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
if: steps.changes.outputs.fw == 'true'
|
||||
@@ -45,7 +41,7 @@ jobs:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.platformio
|
||||
key: pio-${{ runner.os }}-${{ matrix.env }}-${{ hashFiles('firmware/platformio.ini') }}
|
||||
key: pio-${{ runner.os }}-${{ hashFiles('firmware/platformio.ini') }}
|
||||
|
||||
- name: Install PlatformIO
|
||||
if: steps.changes.outputs.fw == 'true'
|
||||
@@ -53,12 +49,58 @@ jobs:
|
||||
python -m pip install -U pip
|
||||
python -m pip install -U platformio
|
||||
|
||||
- name: Build
|
||||
- name: Discover PlatformIO envs
|
||||
id: envs
|
||||
if: steps.changes.outputs.fw == 'true'
|
||||
working-directory: firmware
|
||||
run: pio run -e ${{ matrix.env }}
|
||||
run: |
|
||||
envs="$(pio project config --json-output | python3 -c 'import json,sys
|
||||
arr=json.load(sys.stdin)
|
||||
names=[]
|
||||
for item in arr:
|
||||
name=item.get("name","")
|
||||
if name.startswith("env:"):
|
||||
names.append(name.split("env:",1)[1])
|
||||
print(",".join(names))')"
|
||||
if [[ -z "$envs" ]]; then
|
||||
echo "has_envs=false" >>"$GITHUB_OUTPUT"
|
||||
echo "No PlatformIO env found in firmware/platformio.ini."
|
||||
else
|
||||
echo "has_envs=true" >>"$GITHUB_OUTPUT"
|
||||
echo "envs=$envs" >>"$GITHUB_OUTPUT"
|
||||
echo "Detected envs: $envs"
|
||||
fi
|
||||
|
||||
- name: Test
|
||||
if: steps.changes.outputs.fw == 'true' && matrix.env == 'native'
|
||||
- name: Build all declared envs
|
||||
if: steps.changes.outputs.fw == 'true' && steps.envs.outputs.has_envs == 'true'
|
||||
working-directory: firmware
|
||||
run: pio test -e native
|
||||
run: |
|
||||
IFS=',' read -ra env_arr <<<"${{ steps.envs.outputs.envs }}"
|
||||
for env_name in "${env_arr[@]}"; do
|
||||
echo "==> pio run -e ${env_name}"
|
||||
pio run -e "${env_name}"
|
||||
done
|
||||
|
||||
- name: Unit tests (valid env only)
|
||||
if: steps.changes.outputs.fw == 'true' && steps.envs.outputs.has_envs == 'true'
|
||||
working-directory: firmware
|
||||
run: |
|
||||
if [[ ! -d test ]]; then
|
||||
echo "No firmware/test directory. Skipping pio test."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IFS=',' read -ra env_arr <<<"${{ steps.envs.outputs.envs }}"
|
||||
test_env=""
|
||||
for env_name in "${env_arr[@]}"; do
|
||||
if [[ "$env_name" == "esp32dev" ]]; then
|
||||
test_env="$env_name"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "$test_env" ]]; then
|
||||
test_env="${env_arr[0]}"
|
||||
fi
|
||||
|
||||
echo "==> pio test -e ${test_env} --without-uploading"
|
||||
pio test -e "${test_env}" --without-uploading
|
||||
|
||||
@@ -141,9 +141,7 @@ setup_branch_protection() {
|
||||
contexts+=("Scope Guard / guard")
|
||||
|
||||
if [[ "${REQUIRE_BUILD_CHECKS}" == "1" ]]; then
|
||||
contexts+=("Firmware CI / pio (esp32s3_arduino)")
|
||||
contexts+=("Firmware CI / pio (esp32_arduino)")
|
||||
contexts+=("Firmware CI / pio (native)")
|
||||
contexts+=("Firmware CI / pio")
|
||||
contexts+=("Hardware CI (KiCad) / hw")
|
||||
contexts+=("Compliance Gate / validate")
|
||||
fi
|
||||
|
||||
+13
-7
@@ -29,7 +29,8 @@ avec:
|
||||
- Local only (loopback `127.0.0.1`), pas d'exposition publique.
|
||||
- Secrets hors git (`~/.zeroclaw/env`, mode `600`).
|
||||
- Cout modele borne par quotas webhook locaux.
|
||||
- Pas de flash/upload firmware sans detection hardware prealable.
|
||||
- Flash/upload/serial monitor forces par defaut apres detection hardware valide.
|
||||
- Jamais de commande `pio` sur un env non declare dans `platformio.ini`.
|
||||
|
||||
## Plan par phases
|
||||
|
||||
@@ -73,8 +74,10 @@ Actions:
|
||||
2. Hardware discover: `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
|
||||
3. Build/test firmware repo RTC:
|
||||
- `cd /Users/cils/Documents/Lelectron_rare/RTC_BL_PHONE`
|
||||
- `pio run`
|
||||
- `pio test -e native` (si environnement present)
|
||||
- `pio run -e esp32dev`
|
||||
- `pio run -e esp32dev -t upload`
|
||||
- `pio device monitor -p <port> -b 115200` (capture 60s)
|
||||
- ou commande unique: `tools/ai/zeroclaw_hw_firmware_loop.sh rtc`
|
||||
4. Trace webhook:
|
||||
- `tools/ai/zeroclaw_webhook_send.sh --repo-hint rtc "rtc loop status ..."`
|
||||
|
||||
@@ -91,8 +94,11 @@ Actions:
|
||||
2. Hardware discover: `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
|
||||
3. Build/test firmware repo Zacus:
|
||||
- `cd /Users/cils/Documents/Lelectron_rare/le-mystere-professeur-zacus/hardware/firmware`
|
||||
- `pio run`
|
||||
- `pio test -e native` (si environnement present)
|
||||
- `pio run -e esp32dev`
|
||||
- `pio run -e esp32dev -t upload`
|
||||
- `pio device monitor -p <port> -b 115200` (capture 60s)
|
||||
- ou commande unique: `tools/ai/zeroclaw_hw_firmware_loop.sh zacus`
|
||||
- statut tests: `blocked` tant que dossier `test/` absent
|
||||
4. Trace webhook:
|
||||
- `tools/ai/zeroclaw_webhook_send.sh --repo-hint zacus "zacus loop status ..."`
|
||||
|
||||
@@ -122,7 +128,7 @@ Sortie attendue:
|
||||
|
||||
1. Preflight auth + hardware.
|
||||
2. Stack up + smoke endpoints.
|
||||
3. RTC loop (dev/test + webhook trace).
|
||||
4. Zacus loop (dev/test + webhook trace).
|
||||
3. RTC loop (build + upload + monitor + webhook trace).
|
||||
4. Zacus loop (build + upload + monitor + webhook trace).
|
||||
5. Review logs + correction ciblée.
|
||||
6. Commit/PR petit lot.
|
||||
|
||||
+22
-15
@@ -27,23 +27,26 @@ Format:
|
||||
- AC: commande `prometheus --version` disponible.
|
||||
- Evidence: sortie shell `prometheus, version ...`
|
||||
|
||||
- [ ] T-005 - Stabiliser pairing bearer auto
|
||||
- [x] T-005 - Stabiliser pairing bearer auto
|
||||
- AC: `artifacts/zeroclaw/pair_token.txt` utilisable pour webhook sans override manuel.
|
||||
- Evidence: `tools/ai/zeroclaw_webhook_send.sh --repo-hint rtc "pairing check"`
|
||||
- Status: `2026-02-21` done (401 resolved; gateway now accepts bearer, downstream model may still return HTTP 500).
|
||||
|
||||
## Boucle hardware RTC
|
||||
|
||||
- [ ] T-101 - Discover hardware RTC
|
||||
- [x] T-101 - Discover hardware RTC
|
||||
- AC: au moins un port detecte pour carte RTC.
|
||||
- Evidence: `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
|
||||
|
||||
- [ ] T-102 - Build firmware RTC
|
||||
- AC: `pio run` termine sans erreur bloquante.
|
||||
- [x] T-102 - Build firmware RTC
|
||||
- AC: `pio run -e esp32dev` termine sans erreur bloquante.
|
||||
- Evidence: logs build RTC.
|
||||
- Status: `2026-02-21` done.
|
||||
|
||||
- [ ] T-103 - Test firmware RTC
|
||||
- AC: `pio test` (ou fallback test local) execute.
|
||||
- Evidence: rapport test RTC.
|
||||
- [ ] T-103 - Upload + monitor RTC (forced default)
|
||||
- AC: `pio run -e esp32dev -t upload` puis monitor 60s executes.
|
||||
- Evidence: logs upload/monitor RTC.
|
||||
- Blocker: `esptool` connect timeout (`No serial data received`) on selected USB serial port.
|
||||
|
||||
- [ ] T-104 - Trace webhook RTC
|
||||
- AC: une ligne JSONL avec `repo_hint=rtc` apparait.
|
||||
@@ -51,17 +54,19 @@ Format:
|
||||
|
||||
## Boucle hardware Zacus
|
||||
|
||||
- [ ] T-201 - Discover hardware Zacus
|
||||
- [x] T-201 - Discover hardware Zacus
|
||||
- AC: au moins un port detecte pour carte Zacus.
|
||||
- Evidence: `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
|
||||
|
||||
- [ ] T-202 - Build firmware Zacus
|
||||
- AC: `pio run` (dans `hardware/firmware`) termine sans erreur bloquante.
|
||||
- [x] T-202 - Build firmware Zacus
|
||||
- AC: `pio run -e esp32dev` (dans `hardware/firmware`) termine sans erreur bloquante.
|
||||
- Evidence: logs build Zacus.
|
||||
- Status: `2026-02-21` done.
|
||||
|
||||
- [ ] T-203 - Test firmware Zacus
|
||||
- AC: `pio test` (ou fallback test local) execute.
|
||||
- Evidence: rapport test Zacus.
|
||||
- [ ] T-203 - Upload + monitor Zacus (forced default)
|
||||
- AC: `pio run -e esp32dev -t upload` puis monitor 60s executes.
|
||||
- Evidence: logs upload/monitor Zacus.
|
||||
- Blocker: `esptool` connect timeout (`No serial data received`) on selected USB serial port.
|
||||
|
||||
- [ ] T-204 - Trace webhook Zacus
|
||||
- AC: une ligne JSONL avec `repo_hint=zacus` apparait.
|
||||
@@ -69,13 +74,14 @@ Format:
|
||||
|
||||
## Observabilite et cout
|
||||
|
||||
- [ ] T-301 - Stack endpoints smoke
|
||||
- [x] T-301 - Stack endpoints smoke
|
||||
- AC: `3000/health`, `8788`, `9090/-/ready` tous OK.
|
||||
- Evidence: captures `curl`.
|
||||
|
||||
- [ ] T-302 - Dry-run webhook budget
|
||||
- [x] T-302 - Dry-run webhook budget
|
||||
- AC: `--dry-run` passe sans ecriture execution JSONL.
|
||||
- Evidence: sortie script + diff JSONL.
|
||||
- Status: `2026-02-21` done.
|
||||
|
||||
- [ ] T-303 - Quota call limiter
|
||||
- AC: depassement quota bloque avec code non-zero.
|
||||
@@ -91,3 +97,4 @@ Format:
|
||||
- [ ] Dashboard live exploitable pour suivi continu.
|
||||
- [ ] Prometheus disponible avec target gateway scrapee.
|
||||
- [ ] Logs et preuves archives dans `artifacts/zeroclaw/`.
|
||||
- [ ] Aucune commande documentee n'utilise `-e native` ou `-e test`.
|
||||
|
||||
@@ -4,7 +4,7 @@ project:
|
||||
targets:
|
||||
- esp32s3
|
||||
- esp32
|
||||
- native
|
||||
- esp32dev
|
||||
|
||||
ai:
|
||||
triggers:
|
||||
|
||||
@@ -9,7 +9,7 @@ Run one orchestration layer that can:
|
||||
- converse against `RTC_BL_PHONE` and `le-mystere-professeur-zacus` independently,
|
||||
- keep workspace boundaries strict per repo,
|
||||
- run low-cost autonomous loops with guarded command allowlists,
|
||||
- stay ready for connected hardware checks before any upload/flash action.
|
||||
- enforce upload/flash/serial-monitor loops by default on connected hardware.
|
||||
|
||||
## 2) Scope
|
||||
|
||||
@@ -18,6 +18,7 @@ In scope:
|
||||
- local ZeroClaw profile bootstrap for both repos,
|
||||
- deterministic workspace switch (`rtc` vs `zacus`) from one CLI entrypoint,
|
||||
- hardware discovery/introspection preflight,
|
||||
- forced-by-default firmware loop (build + upload + monitor) per repo target,
|
||||
- lightweight CI validation for orchestration scripts/spec.
|
||||
|
||||
Out of scope:
|
||||
@@ -76,6 +77,7 @@ This keeps `autonomy.workspace_only = true` effective on a per-repo boundary.
|
||||
- generates live follow dashboard at `http://127.0.0.1:8788/`,
|
||||
- dashboard includes live polling panels for `/conversations.jsonl` and `/gateway.log` (1s polling),
|
||||
- preserves direct raw links: `/conversations.jsonl` and `/gateway.log`,
|
||||
- starts `tools/ai/zeroclaw_watch_1min.sh` watcher and exposes `/realtime_1min.log`,
|
||||
- writes `artifacts/zeroclaw/prometheus.yml` scrape config,
|
||||
- supports local Prometheus startup via `ZEROCLAW_PROM_MODE` (`off`, `auto`, `binary`, `docker`) with `auto` fallback `binary -> docker`,
|
||||
- on macOS, auto-attempts Docker Desktop startup before Prometheus docker mode,
|
||||
@@ -83,7 +85,19 @@ This keeps `autonomy.workspace_only = true` effective on a per-repo boundary.
|
||||
- `tools/ai/zeroclaw_stack_down.sh`
|
||||
- stops local gateway/follow processes,
|
||||
- stops local Prometheus process/container if managed by the stack,
|
||||
- stops `tools/ai/zeroclaw_watch_1min.sh` watcher process,
|
||||
- confirms logs remain in `artifacts/zeroclaw/`.
|
||||
- `tools/ai/zeroclaw_watch_1min.sh`
|
||||
- supports `start|stop|status|run|once`,
|
||||
- appends one status line every 60s by default to `artifacts/zeroclaw/realtime_1min.log`,
|
||||
- line format:
|
||||
- `<ts> | paired=<...> | uptime=<...> | prom=<...> | convo=<last line> | gateway=<last line>`
|
||||
- `tools/ai/zeroclaw_hw_firmware_loop.sh`
|
||||
- target switch `rtc|zacus`,
|
||||
- validates `platformio.ini` env exists before running,
|
||||
- forces `build -> upload -> serial monitor` by default,
|
||||
- auto-detects serial port when not specified,
|
||||
- monitor timeout default 60s.
|
||||
- `tools/ai/zeroclaw_webhook_send.sh`
|
||||
- sends webhook by default (no mandatory allow flag),
|
||||
- keeps `--allow-model-call` as backward-compatible legacy option,
|
||||
@@ -148,6 +162,7 @@ Raw URLs:
|
||||
|
||||
- `http://127.0.0.1:8788/conversations.jsonl`
|
||||
- `http://127.0.0.1:8788/gateway.log`
|
||||
- `http://127.0.0.1:8788/realtime_1min.log`
|
||||
- `http://127.0.0.1:8788/prometheus.yml`
|
||||
|
||||
Conversation JSONL line schema (append-only):
|
||||
@@ -203,3 +218,18 @@ Suggested contents:
|
||||
Behavior:
|
||||
|
||||
- `zeroclaw_dual_chat.sh`, `zeroclaw_stack_up.sh`, and `zeroclaw_webhook_send.sh` auto-load this file if present.
|
||||
|
||||
## 11) Firmware Loop Defaults (local hardware)
|
||||
|
||||
Default execution policy:
|
||||
|
||||
- with hardware connected, upload/flash and serial monitor are forced by default.
|
||||
- if no serial port is detected, loop fails fast (non-zero) instead of silently skipping upload.
|
||||
- only declared PlatformIO envs are allowed; no `-e native` or `-e test` shortcuts.
|
||||
|
||||
Default commands:
|
||||
|
||||
- RTC:
|
||||
- `tools/ai/zeroclaw_hw_firmware_loop.sh rtc`
|
||||
- Zacus:
|
||||
- `tools/ai/zeroclaw_hw_firmware_loop.sh zacus`
|
||||
|
||||
@@ -11,32 +11,34 @@ Last updated: 2026-02-21
|
||||
|
||||
## Daily autonomous sequence
|
||||
|
||||
- [ ] D-001 - `tools/ai/zeroclaw_stack_down.sh` then `ZEROCLAW_PROM_MODE=auto tools/ai/zeroclaw_stack_up.sh`.
|
||||
- [ ] D-002 - Smoke endpoints:
|
||||
- [x] D-001 - `tools/ai/zeroclaw_stack_down.sh` then `ZEROCLAW_PROM_MODE=auto tools/ai/zeroclaw_stack_up.sh`.
|
||||
- [x] D-002 - Smoke endpoints:
|
||||
- `curl -fsS http://127.0.0.1:3000/health`
|
||||
- `curl -fsS http://127.0.0.1:8788/`
|
||||
- `curl -fsS http://127.0.0.1:9090/-/ready`
|
||||
- [ ] D-003 - RTC loop:
|
||||
- `tools/ai/zeroclaw_dual_chat.sh rtc --provider-check`
|
||||
- `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
|
||||
- repo build/test
|
||||
- `tools/ai/zeroclaw_hw_firmware_loop.sh rtc` (build+upload+monitor forced default)
|
||||
- webhook trace with `--repo-hint rtc`
|
||||
- current blocker: serial upload timeout (`No serial data received`)
|
||||
- [ ] D-004 - Zacus loop:
|
||||
- `tools/ai/zeroclaw_dual_chat.sh zacus --provider-check`
|
||||
- `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
|
||||
- repo build/test
|
||||
- `tools/ai/zeroclaw_hw_firmware_loop.sh zacus` (build+upload+monitor forced default)
|
||||
- webhook trace with `--repo-hint zacus`
|
||||
- current blocker: serial upload timeout (`No serial data received`)
|
||||
- [ ] D-005 - Review `artifacts/zeroclaw/gateway.log` + `conversations.jsonl`.
|
||||
|
||||
## Hardware safety gates
|
||||
|
||||
- [ ] H-001 - No flash/upload if `--hardware` detect returns no board.
|
||||
- [ ] H-001 - Flash/upload/monitor are forced by default when a board is detected.
|
||||
- [ ] H-002 - Resolve stable serial target before upload.
|
||||
- [ ] H-003 - Keep per-run logs under `artifacts/zeroclaw/`.
|
||||
|
||||
## Cost/control gates
|
||||
|
||||
- [ ] C-001 - Validate `tools/ai/zeroclaw_webhook_send.sh --dry-run`.
|
||||
- [x] C-001 - Validate `tools/ai/zeroclaw_webhook_send.sh --dry-run`.
|
||||
- [ ] C-002 - Validate hourly quota guard (`ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR`).
|
||||
- [ ] C-003 - Validate message length guard (`ZEROCLAW_WEBHOOK_MAX_CHARS`).
|
||||
|
||||
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
RTC_REPO="${ZEROCLAW_RTC_REPO:-/Users/cils/Documents/Lelectron_rare/RTC_BL_PHONE}"
|
||||
ZACUS_REPO="${ZEROCLAW_ZACUS_REPO:-/Users/cils/Documents/Lelectron_rare/le-mystere-professeur-zacus}"
|
||||
|
||||
RTC_FW_DIR="$RTC_REPO"
|
||||
ZACUS_FW_DIR="$ZACUS_REPO/hardware/firmware"
|
||||
|
||||
RTC_ENV_DEFAULT="${ZEROCLAW_RTC_PIO_ENV:-esp32dev}"
|
||||
ZACUS_ENV_DEFAULT="${ZEROCLAW_ZACUS_PIO_ENV:-esp32dev}"
|
||||
MONITOR_SECS_DEFAULT="${ZEROCLAW_MONITOR_SECS:-60}"
|
||||
BAUD_DEFAULT="${ZEROCLAW_SERIAL_BAUD:-115200}"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage:
|
||||
$(basename "$0") <rtc|zacus> [--env <platformio_env>] [--port <serial_port>] [--baud <baud>] [--monitor-secs <n>]
|
||||
|
||||
Behavior:
|
||||
- Build is always executed.
|
||||
- Upload/flash is forced by default.
|
||||
- Serial monitor is forced by default for monitor-secs (default: ${MONITOR_SECS_DEFAULT}s).
|
||||
USAGE
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
target="$1"
|
||||
shift
|
||||
|
||||
FW_DIR=""
|
||||
ENV_NAME=""
|
||||
case "$target" in
|
||||
rtc)
|
||||
FW_DIR="$RTC_FW_DIR"
|
||||
ENV_NAME="$RTC_ENV_DEFAULT"
|
||||
;;
|
||||
zacus)
|
||||
FW_DIR="$ZACUS_FW_DIR"
|
||||
ENV_NAME="$ZACUS_ENV_DEFAULT"
|
||||
;;
|
||||
*)
|
||||
echo "[fail] unsupported target: $target (expected rtc|zacus)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
PORT="${ZEROCLAW_UPLOAD_PORT:-}"
|
||||
BAUD="$BAUD_DEFAULT"
|
||||
MONITOR_SECS="$MONITOR_SECS_DEFAULT"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env)
|
||||
ENV_NAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--port)
|
||||
PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--baud)
|
||||
BAUD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--monitor-secs)
|
||||
MONITOR_SECS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[fail] unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d "$FW_DIR" ]]; then
|
||||
echo "[fail] firmware directory not found: $FW_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$FW_DIR/platformio.ini" ]]; then
|
||||
echo "[fail] platformio.ini not found in: $FW_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v pio >/dev/null 2>&1; then
|
||||
echo "[fail] platformio (pio) not found in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! rg -q "^\[env:${ENV_NAME}\]" "$FW_DIR/platformio.ini"; then
|
||||
echo "[fail] invalid PlatformIO env '${ENV_NAME}' for $target." >&2
|
||||
echo "[info] valid envs:" >&2
|
||||
rg "^\[env:" "$FW_DIR/platformio.ini" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$PORT" ]]; then
|
||||
if ! PORT="$(
|
||||
cd "$FW_DIR"
|
||||
pio device list --json-output 2>/dev/null | python3 -c 'import json,sys
|
||||
raw=sys.stdin.read().strip()
|
||||
try:
|
||||
arr=json.loads(raw) if raw else []
|
||||
except Exception:
|
||||
arr=[]
|
||||
best=None
|
||||
best_score=-10**9
|
||||
for d in arr:
|
||||
p=str(d.get("port",""))
|
||||
desc=str(d.get("description",""))
|
||||
hwid=str(d.get("hwid",""))
|
||||
if not p:
|
||||
continue
|
||||
score=0
|
||||
low=f\"{p} {desc} {hwid}\".lower()
|
||||
if hwid and hwid.lower() != \"n/a\":
|
||||
score += 50
|
||||
if \"usb\" in low:
|
||||
score += 30
|
||||
if \"cp210\" in low or \"ch340\" in low or \"uart\" in low:
|
||||
score += 20
|
||||
if \"bluetooth\" in low:
|
||||
score -= 100
|
||||
if \"/cu.urt\" in p.lower() and hwid.lower() == \"n/a\":
|
||||
score -= 120
|
||||
if score > best_score:
|
||||
best_score=score
|
||||
best=p
|
||||
if best:
|
||||
print(best)
|
||||
raise SystemExit(0)
|
||||
raise SystemExit(1)'
|
||||
)"; then
|
||||
PORT=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$PORT" ]]; then
|
||||
echo "[fail] no serial port detected. Upload/monitor are forced by default." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
UPLOAD_ARGS=()
|
||||
MONITOR_ARGS=()
|
||||
if [[ -n "$PORT" ]]; then
|
||||
UPLOAD_ARGS+=(--upload-port "$PORT")
|
||||
MONITOR_ARGS+=(-p "$PORT")
|
||||
fi
|
||||
if [[ -n "$BAUD" ]]; then
|
||||
MONITOR_ARGS+=(-b "$BAUD")
|
||||
fi
|
||||
|
||||
echo "[info] target=$target fw_dir=$FW_DIR env=$ENV_NAME port=$PORT baud=$BAUD monitor_secs=$MONITOR_SECS"
|
||||
echo "[step] build"
|
||||
(cd "$FW_DIR" && pio run -e "$ENV_NAME")
|
||||
|
||||
echo "[step] upload/flash (forced default)"
|
||||
(cd "$FW_DIR" && pio run -e "$ENV_NAME" -t upload "${UPLOAD_ARGS[@]}")
|
||||
|
||||
echo "[step] serial monitor (forced default)"
|
||||
python3 - "$FW_DIR" "$MONITOR_SECS" "${MONITOR_ARGS[@]}" <<'PY'
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
fw_dir = sys.argv[1]
|
||||
monitor_secs = int(sys.argv[2])
|
||||
monitor_args = sys.argv[3:]
|
||||
cmd = ["pio", "device", "monitor", *monitor_args]
|
||||
print(f"[info] monitor command: {' '.join(cmd)} (timeout={monitor_secs}s)")
|
||||
|
||||
proc = subprocess.Popen(cmd, cwd=fw_dir, preexec_fn=os.setsid)
|
||||
try:
|
||||
time.sleep(monitor_secs)
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
os.killpg(proc.pid, signal.SIGTERM)
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.wait(timeout=2)
|
||||
PY
|
||||
|
||||
echo "[ok] loop complete for $target ($ENV_NAME)."
|
||||
@@ -2,6 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
ART_DIR="${ZEROCLAW_ART_DIR:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/artifacts/zeroclaw}"
|
||||
ROOT_DIR="/Users/cils/Documents/Lelectron_rare/Kill_LIFE"
|
||||
GW_PID_FILE="$ART_DIR/gateway.pid"
|
||||
FW_PID_FILE="$ART_DIR/follow.pid"
|
||||
PROM_PID_FILE="$ART_DIR/prometheus.pid"
|
||||
@@ -9,6 +10,7 @@ PROM_CONTAINER="${ZEROCLAW_PROM_CONTAINER:-zeroclaw-prometheus}"
|
||||
GW_MANAGED_FILE="$ART_DIR/gateway.managed"
|
||||
FW_MANAGED_FILE="$ART_DIR/follow.managed"
|
||||
PROM_MANAGED_FILE="$ART_DIR/prometheus.managed"
|
||||
WATCHER_SCRIPT="$ROOT_DIR/tools/ai/zeroclaw_watch_1min.sh"
|
||||
|
||||
stop_pid_file() {
|
||||
local pid_file="$1"
|
||||
@@ -42,4 +44,7 @@ if [[ -f "$PROM_MANAGED_FILE" ]]; then
|
||||
fi
|
||||
fi
|
||||
rm -f "$GW_MANAGED_FILE" "$FW_MANAGED_FILE" "$PROM_MANAGED_FILE"
|
||||
if [[ -x "$WATCHER_SCRIPT" ]]; then
|
||||
"$WATCHER_SCRIPT" stop >/dev/null 2>&1 || true
|
||||
fi
|
||||
echo "ZeroClaw local stack stopped. Logs preserved in artifacts/zeroclaw/."
|
||||
|
||||
@@ -27,6 +27,7 @@ PROM_SCRAPE_INTERVAL="${ZEROCLAW_PROM_SCRAPE_INTERVAL:-15s}"
|
||||
PROM_CONTAINER="${ZEROCLAW_PROM_CONTAINER:-zeroclaw-prometheus}"
|
||||
DOCKER_WAIT_SECS="${ZEROCLAW_DOCKER_WAIT_SECS:-90}"
|
||||
PROM_READY_WAIT_SECS="${ZEROCLAW_PROM_READY_WAIT_SECS:-15}"
|
||||
AUTO_REPAIR_ON_INVALID_TOKEN="${ZEROCLAW_AUTO_REPAIR_ON_INVALID_TOKEN:-1}"
|
||||
|
||||
GW_PID_FILE="$ART_DIR/gateway.pid"
|
||||
FW_PID_FILE="$ART_DIR/follow.pid"
|
||||
@@ -40,13 +41,17 @@ PROM_LOG="$ART_DIR/prometheus.log"
|
||||
PROM_CONFIG_FILE="$ART_DIR/prometheus.yml"
|
||||
PROM_DATA_DIR="$ART_DIR/prometheus-data"
|
||||
PROM_STATUS="disabled"
|
||||
ZEROCLAW_CONFIG_FILE="${ZEROCLAW_CONFIG_FILE:-$HOME/.zeroclaw/config.toml}"
|
||||
GW_MANAGED_FILE="$ART_DIR/gateway.managed"
|
||||
FW_MANAGED_FILE="$ART_DIR/follow.managed"
|
||||
PROM_MANAGED_FILE="$ART_DIR/prometheus.managed"
|
||||
WATCHER_SCRIPT="$ROOT_DIR/tools/ai/zeroclaw_watch_1min.sh"
|
||||
RT_LOG="$ART_DIR/realtime_1min.log"
|
||||
|
||||
mkdir -p "$ART_DIR"
|
||||
touch "$CONVO_FILE"
|
||||
touch "$GW_LOG"
|
||||
touch "$RT_LOG"
|
||||
|
||||
if [[ -f "$HOME/.zeroclaw/config.toml" ]]; then
|
||||
chmod 600 "$HOME/.zeroclaw/config.toml" >/dev/null 2>&1 || true
|
||||
@@ -211,8 +216,44 @@ sys.exit(0 if obj.get("paired") else 1)' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
read_pair_token() {
|
||||
[[ -f "$TOKEN_FILE" ]] || return 1
|
||||
cat "$TOKEN_FILE" 2>/dev/null
|
||||
if [[ -f "$TOKEN_FILE" ]]; then
|
||||
cat "$TOKEN_FILE" 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$ZEROCLAW_CONFIG_FILE" ]]; then
|
||||
local token_from_config
|
||||
token_from_config="$(python3 - "$ZEROCLAW_CONFIG_FILE" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
cfg = Path(sys.argv[1])
|
||||
try:
|
||||
import tomllib
|
||||
except Exception:
|
||||
print("", end="")
|
||||
raise SystemExit(0)
|
||||
|
||||
try:
|
||||
obj = tomllib.loads(cfg.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
print("", end="")
|
||||
raise SystemExit(0)
|
||||
|
||||
tokens = obj.get("gateway", {}).get("paired_tokens", [])
|
||||
if isinstance(tokens, list) and tokens:
|
||||
print(str(tokens[0]), end="")
|
||||
PY
|
||||
)"
|
||||
if [[ -n "$token_from_config" ]]; then
|
||||
printf '%s\n' "$token_from_config" >"$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
printf '%s\n' "$token_from_config"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
token_is_usable() {
|
||||
@@ -231,6 +272,34 @@ token_is_usable() {
|
||||
return 0
|
||||
}
|
||||
|
||||
wait_for_gateway_health() {
|
||||
local waited=0
|
||||
while (( waited < 20 )); do
|
||||
if curl -fsS "http://$GATEWAY_HOST:$GATEWAY_PORT/health" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
clear_config_paired_tokens() {
|
||||
[[ -f "$ZEROCLAW_CONFIG_FILE" ]] || return 1
|
||||
python3 - "$ZEROCLAW_CONFIG_FILE" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
cfg = Path(sys.argv[1])
|
||||
text = cfg.read_text(encoding="utf-8")
|
||||
updated, n = re.subn(r'(?m)^paired_tokens\s*=\s*\[.*\]\s*$', 'paired_tokens = []', text)
|
||||
if n == 0:
|
||||
raise SystemExit(1)
|
||||
cfg.write_text(updated, encoding="utf-8")
|
||||
PY
|
||||
}
|
||||
|
||||
extract_pair_code() {
|
||||
grep -Eo 'X-Pairing-Code: [0-9]{6}' "$GW_LOG" 2>/dev/null | tail -1 | awk '{print $2}' || true
|
||||
}
|
||||
@@ -281,6 +350,36 @@ ensure_gateway_pairing() {
|
||||
current_token="$(read_pair_token || true)"
|
||||
if ! token_is_usable "$current_token"; then
|
||||
rm -f "$TOKEN_FILE"
|
||||
|
||||
if [[ "$AUTO_REPAIR_ON_INVALID_TOKEN" == "1" && -f "$GW_MANAGED_FILE" ]]; then
|
||||
if clear_config_paired_tokens; then
|
||||
local gw_pid=""
|
||||
gw_pid="$(cat "$GW_PID_FILE" 2>/dev/null || true)"
|
||||
if [[ -n "$gw_pid" ]] && kill -0 "$gw_pid" >/dev/null 2>&1; then
|
||||
kill "$gw_pid" >/dev/null 2>&1 || true
|
||||
sleep 0.3
|
||||
if kill -0 "$gw_pid" >/dev/null 2>&1; then
|
||||
kill -9 "$gw_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
nohup "$ZEROCLAW_BIN" gateway --port "$GATEWAY_PORT" --host "$GATEWAY_HOST" >"$GW_LOG" 2>&1 &
|
||||
echo "$!" >"$GW_PID_FILE"
|
||||
wait_for_gateway_health || true
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
pair_code="$(extract_pair_code)"
|
||||
if [[ -n "$pair_code" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
if [[ -n "${pair_code:-}" ]]; then
|
||||
write_pair_token_from_code "$pair_code" || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -461,6 +560,7 @@ cat >"$INDEX_FILE" <<EOF
|
||||
<div class="links">
|
||||
<a href="/conversations.jsonl">/conversations.jsonl</a>
|
||||
<a href="/gateway.log">/gateway.log</a>
|
||||
<a href="/realtime_1min.log">/realtime_1min.log</a>
|
||||
<a href="/prometheus.yml">/prometheus.yml</a>
|
||||
<a href="http://$GATEWAY_HOST:$GATEWAY_PORT/health">/health</a>
|
||||
<a href="http://$GATEWAY_HOST:$GATEWAY_PORT/metrics">/metrics</a>
|
||||
@@ -576,9 +676,14 @@ cat >"$INDEX_FILE" <<EOF
|
||||
</html>
|
||||
EOF
|
||||
|
||||
if [[ -x "$WATCHER_SCRIPT" ]]; then
|
||||
"$WATCHER_SCRIPT" start >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "Gateway: http://$GATEWAY_HOST:$GATEWAY_PORT/health"
|
||||
echo "Follow : http://127.0.0.1:$FOLLOW_PORT/"
|
||||
echo "Prom : http://$PROM_HOST:$PROM_PORT/targets ($PROM_STATUS)"
|
||||
echo "RT 1m : http://127.0.0.1:$FOLLOW_PORT/realtime_1min.log"
|
||||
echo "Logs : $GW_LOG"
|
||||
echo "Token : $TOKEN_FILE"
|
||||
if [[ ! -s "$TOKEN_FILE" ]]; then
|
||||
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="/Users/cils/Documents/Lelectron_rare/Kill_LIFE"
|
||||
ART_DIR="${ZEROCLAW_ART_DIR:-$ROOT_DIR/artifacts/zeroclaw}"
|
||||
HOST="${ZEROCLAW_GATEWAY_HOST:-127.0.0.1}"
|
||||
PORT="${ZEROCLAW_GATEWAY_PORT:-3000}"
|
||||
PROM_HOST="${ZEROCLAW_PROM_HOST:-127.0.0.1}"
|
||||
PROM_PORT="${ZEROCLAW_PROM_PORT:-9090}"
|
||||
INTERVAL_SECS="${ZEROCLAW_RT_INTERVAL_SECS:-60}"
|
||||
|
||||
PID_FILE="$ART_DIR/realtime_1min.pid"
|
||||
LOG_FILE="$ART_DIR/realtime_1min.log"
|
||||
CONVO_FILE="$ART_DIR/conversations.jsonl"
|
||||
GW_LOG_FILE="$ART_DIR/gateway.log"
|
||||
|
||||
mkdir -p "$ART_DIR"
|
||||
touch "$LOG_FILE" "$CONVO_FILE" "$GW_LOG_FILE"
|
||||
|
||||
is_running() {
|
||||
[[ -f "$PID_FILE" ]] || return 1
|
||||
local pid
|
||||
pid="$(cat "$PID_FILE" 2>/dev/null || true)"
|
||||
[[ -n "$pid" ]] || return 1
|
||||
kill -0 "$pid" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
collect_once() {
|
||||
local ts health paired uptime prom last_convo last_gw
|
||||
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
|
||||
health="$(curl -sS "http://$HOST:$PORT/health" || true)"
|
||||
if [[ -n "$health" ]]; then
|
||||
paired="$(printf '%s' "$health" | python3 -c 'import json,sys
|
||||
raw=sys.stdin.read().strip()
|
||||
try:
|
||||
obj=json.loads(raw) if raw else {}
|
||||
except Exception:
|
||||
print("unknown")
|
||||
raise SystemExit
|
||||
print(str(obj.get("paired","unknown")).lower())' 2>/dev/null || echo "unknown")"
|
||||
uptime="$(printf '%s' "$health" | python3 -c 'import json,sys
|
||||
raw=sys.stdin.read().strip()
|
||||
try:
|
||||
obj=json.loads(raw) if raw else {}
|
||||
except Exception:
|
||||
print("unknown")
|
||||
raise SystemExit
|
||||
print(obj.get("runtime",{}).get("uptime_seconds","unknown"))' 2>/dev/null || echo "unknown")"
|
||||
else
|
||||
paired="down"
|
||||
uptime="down"
|
||||
fi
|
||||
|
||||
if curl -fsS "http://$PROM_HOST:$PROM_PORT/-/ready" >/dev/null 2>&1; then
|
||||
prom="ready"
|
||||
else
|
||||
prom="down"
|
||||
fi
|
||||
|
||||
last_convo="$(tail -n 1 "$CONVO_FILE" 2>/dev/null || true)"
|
||||
last_gw="$(tail -n 1 "$GW_LOG_FILE" 2>/dev/null || true)"
|
||||
last_convo="${last_convo//$'\t'/ }"
|
||||
last_gw="${last_gw//$'\t'/ }"
|
||||
|
||||
printf '%s | paired=%s | uptime=%s | prom=%s | convo=%s | gateway=%s\n' \
|
||||
"$ts" "$paired" "$uptime" "$prom" "$last_convo" "$last_gw" >>"$LOG_FILE"
|
||||
}
|
||||
|
||||
run_loop() {
|
||||
while true; do
|
||||
collect_once
|
||||
sleep "$INTERVAL_SECS"
|
||||
done
|
||||
}
|
||||
|
||||
start() {
|
||||
if is_running; then
|
||||
echo "[info] watcher already running (pid $(cat "$PID_FILE"))."
|
||||
return 0
|
||||
fi
|
||||
nohup "$0" run >>"$LOG_FILE" 2>&1 &
|
||||
echo "$!" >"$PID_FILE"
|
||||
echo "[ok] watcher started (pid $(cat "$PID_FILE"))."
|
||||
}
|
||||
|
||||
stop() {
|
||||
if ! is_running; then
|
||||
rm -f "$PID_FILE"
|
||||
echo "[info] watcher not running."
|
||||
return 0
|
||||
fi
|
||||
local pid
|
||||
pid="$(cat "$PID_FILE")"
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
sleep 0.2
|
||||
if kill -0 "$pid" >/dev/null 2>&1; then
|
||||
kill -9 "$pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
echo "[ok] watcher stopped."
|
||||
}
|
||||
|
||||
status() {
|
||||
if is_running; then
|
||||
echo "[ok] watcher running (pid $(cat "$PID_FILE"))."
|
||||
else
|
||||
echo "[info] watcher not running."
|
||||
fi
|
||||
tail -n 5 "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
start) start ;;
|
||||
stop) stop ;;
|
||||
status) status ;;
|
||||
run) run_loop ;;
|
||||
once) collect_once ;;
|
||||
*)
|
||||
echo "Usage: $(basename "$0") [start|stop|status|run|once]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -17,6 +17,7 @@ ART_DIR="${ZEROCLAW_ART_DIR:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/arti
|
||||
HOST="${ZEROCLAW_GATEWAY_HOST:-127.0.0.1}"
|
||||
PORT="${ZEROCLAW_GATEWAY_PORT:-3000}"
|
||||
TOKEN_FILE="$ART_DIR/pair_token.txt"
|
||||
ZEROCLAW_CONFIG_FILE="${ZEROCLAW_CONFIG_FILE:-$HOME/.zeroclaw/config.toml}"
|
||||
CONVO_FILE="$ART_DIR/conversations.jsonl"
|
||||
BUDGET_FILE="$ART_DIR/webhook_budget.json"
|
||||
MAX_CALLS_PER_HOUR="${ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR:-40}"
|
||||
@@ -93,6 +94,34 @@ TOKEN="${ZEROCLAW_BEARER:-}"
|
||||
if [[ -z "$TOKEN" && -f "$TOKEN_FILE" ]]; then
|
||||
TOKEN="$(cat "$TOKEN_FILE")"
|
||||
fi
|
||||
if [[ -z "$TOKEN" && -f "$ZEROCLAW_CONFIG_FILE" ]]; then
|
||||
TOKEN="$(python3 - "$ZEROCLAW_CONFIG_FILE" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
cfg = Path(sys.argv[1])
|
||||
try:
|
||||
import tomllib
|
||||
except Exception:
|
||||
print("", end="")
|
||||
raise SystemExit(0)
|
||||
|
||||
try:
|
||||
obj = tomllib.loads(cfg.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
print("", end="")
|
||||
raise SystemExit(0)
|
||||
|
||||
tokens = obj.get("gateway", {}).get("paired_tokens", [])
|
||||
if isinstance(tokens, list) and tokens:
|
||||
print(str(tokens[0]), end="")
|
||||
PY
|
||||
)"
|
||||
if [[ -n "$TOKEN" ]]; then
|
||||
printf '%s\n' "$TOKEN" >"$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$ART_DIR"
|
||||
touch "$CONVO_FILE"
|
||||
|
||||
Reference in New Issue
Block a user