Implement S7 operator connect helpers for SaaS and channels.
Add dry-run-default connect dispatcher and per-integration scripts with gitignored local capability state, docs, and unit tests. Mutations require --apply and use nemohermes/openshell only.
This commit is contained in:
+29
-3
@@ -1,6 +1,6 @@
|
||||
# Host scripts
|
||||
|
||||
**Status:** S0b–S6 implemented. S7 pending.
|
||||
**Status:** S0b–S7 implemented.
|
||||
|
||||
All scripts wrap **`nemohermes` / `openshell` / Docker**. No parallel control API.
|
||||
|
||||
@@ -12,11 +12,16 @@ All scripts wrap **`nemohermes` / `openshell` / Docker**. No parallel control AP
|
||||
| `install.sh` | Staged installer (S0b–S5) | ✅ S0b–S5 |
|
||||
| `install/s1-env.sh` | S1: create/validate `.env` | ✅ S1 |
|
||||
| `install/s2-models.sh` | S2: model + vision config + smoke | ✅ S2 |
|
||||
| `install/s4-sandbox.sh` | S4: sandbox verify (attach) or onboard | ✅ S4 |
|
||||
| `install/s4-sandbox.sh` | S4: sandbox verify/onboard | ✅ S4 |
|
||||
| `install/s5-policy-skills.sh` | S5: policy overlays + skills sync | ✅ S5 |
|
||||
| `doctor.sh` | Health checks (Docker, CLIs, sandbox, policy, skills, inference) | ✅ S6 |
|
||||
| `connect.sh` | S7: Operator connect dispatcher | ✅ S7 |
|
||||
| `connect/connect-name.sh` | S7: Name / profile setup | ✅ S7 |
|
||||
| `connect/connect-channels.sh` | S7: Messaging channels (WhatsApp/Email/Telegram) | ✅ S7 |
|
||||
| `connect/connect-square.sh` | S7: Square (remote MCP) | ✅ S7 |
|
||||
| `connect/connect-quickbooks.sh` | S7: QuickBooks Online (local MCP) | ✅ S7 |
|
||||
| `connect/connect-vagaro.sh` | S7: Vagaro (REST + webhooks) | ✅ S7 |
|
||||
| `upgrade.sh` | Snapshot, pull pins, migrate, re-apply policy, doctor | ⏳ Pending |
|
||||
| `connect/*.sh` | Operator connect helpers (Square, QBO, Vagaro, channels) | ⏳ Pending |
|
||||
|
||||
## Shared library
|
||||
|
||||
@@ -25,6 +30,7 @@ All scripts wrap **`nemohermes` / `openshell` / Docker**. No parallel control AP
|
||||
| `lib/common.sh` | Logging, CLI detection, env loading, repo root |
|
||||
| `lib/env.sh` | `.env` validation and creation helpers |
|
||||
| `lib/vision_smoke.sh` | Vision capability smoke test |
|
||||
| `lib/connect_state.sh` | S7: Local capability state management |
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -46,6 +52,14 @@ All scripts wrap **`nemohermes` / `openshell` / Docker**. No parallel control AP
|
||||
./scripts/doctor.sh # human-readable
|
||||
./scripts/doctor.sh --json # machine-readable
|
||||
|
||||
# S7: Connect (default --dry-run; use --apply for mutations)
|
||||
./scripts/connect.sh --help
|
||||
./scripts/connect.sh status
|
||||
./scripts/connect.sh square --dry-run
|
||||
./scripts/connect.sh square --apply
|
||||
./scripts/connect.sh channels --target whatsapp --apply
|
||||
./scripts/connect.sh all --dry-run
|
||||
|
||||
# Or via Make
|
||||
make bootstrap
|
||||
make install
|
||||
@@ -54,8 +68,20 @@ make install-s2
|
||||
make install-s3-s5
|
||||
make install-s5
|
||||
make doctor
|
||||
make connect
|
||||
make connect-status
|
||||
make connect-square
|
||||
make connect-all
|
||||
```
|
||||
|
||||
## S7 Safety model
|
||||
|
||||
- **`--dry-run` is the default** — preview actions without executing
|
||||
- **`--apply`** — execute mutations (prompts for credentials when needed)
|
||||
- **No secrets in git** — credentials stored via OpenShell provider store
|
||||
- **`.local/` directory** — connection state stored in `.local/capability_state.json` (gitignored)
|
||||
- **Owner-safe** — owner never receives terminal/Docker/nano instructions
|
||||
|
||||
## Design reference
|
||||
|
||||
See [docs/INSTALL.md](../docs/INSTALL.md), [docs/UPGRADE.md](../docs/UPGRADE.md), [design/updates-lifecycle.md](../design/updates-lifecycle.md).
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/connect.sh — S7: Operator connect dispatcher
|
||||
#
|
||||
# Dispatches to per-integration connect helpers.
|
||||
# All mutations require --apply; default is --dry-run for safety.
|
||||
#
|
||||
# Platform-first: all mutations via nemohermes / openshell.
|
||||
# Owner never receives terminal/Docker/nano instructions.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/connect.sh --help
|
||||
# ./scripts/connect.sh status
|
||||
# ./scripts/connect.sh name --dry-run
|
||||
# ./scripts/connect.sh name --apply
|
||||
# ./scripts/connect.sh channels --target whatsapp --dry-run
|
||||
# ./scripts/connect.sh square --dry-run
|
||||
# ./scripts/connect.sh square --apply
|
||||
# ./scripts/connect.sh quickbooks --dry-run
|
||||
# ./scripts/connect.sh vagaro --dry-run
|
||||
# ./scripts/connect.sh all --dry-run
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
# shellcheck source=lib/env.sh
|
||||
source "$SCRIPT_DIR/lib/env.sh"
|
||||
# shellcheck source=lib/connect_state.sh
|
||||
source "$SCRIPT_DIR/lib/connect_state.sh"
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
TARGET=""
|
||||
SUBCOMMAND=""
|
||||
EXTRA_ARGS=()
|
||||
|
||||
# ── Usage ──────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [SUBCOMMAND] [OPTIONS]
|
||||
|
||||
S7: Operator connect helpers — wire the owner's SaaS and channels.
|
||||
|
||||
Safety model: --dry-run is the default. Use --apply to perform mutations.
|
||||
--dry-run Preview actions without executing (default)
|
||||
--apply Execute mutations (prompts for credentials when needed)
|
||||
|
||||
Subcommands:
|
||||
status Show current connection status
|
||||
name [--dry-run|--apply] Name / profile setup (sandbox rename guidance)
|
||||
channels [--target <ch>] Connect messaging channels
|
||||
--target whatsapp|email|telegram
|
||||
square [--dry-run|--apply] Connect Square (remote MCP)
|
||||
quickbooks [--dry-run|--apply] Connect QuickBooks Online (local MCP)
|
||||
vagaro [--dry-run|--apply] Connect Vagaro (REST + webhooks)
|
||||
all [--dry-run|--apply] Walk all targets sequentially
|
||||
|
||||
Options:
|
||||
--help Show this help
|
||||
--dry-run Preview only (default)
|
||||
--apply Execute mutations
|
||||
--target <name> Target channel (for 'channels' subcommand)
|
||||
|
||||
Examples:
|
||||
$(basename "$0") status
|
||||
$(basename "$0") square --dry-run
|
||||
$(basename "$0") square --apply
|
||||
$(basename "$0") channels --target telegram --dry-run
|
||||
$(basename "$0") all --dry-run
|
||||
|
||||
State:
|
||||
Connection state is stored in .local/capability_state.json (gitignored).
|
||||
Fixtures under data/fixtures/setup/ are NOT modified.
|
||||
|
||||
Platform-first:
|
||||
All mutations use nemohermes / openshell. No parallel control API.
|
||||
Owner never receives terminal/Docker/nano instructions.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
DRY_RUN=0
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
--target)
|
||||
shift
|
||||
TARGET="${1:-}"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
log_error "--target requires a value (whatsapp|email|telegram)"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
status|name|channels|square|quickbooks|vagaro|all)
|
||||
SUBCOMMAND="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
# Collect remaining args for subcommand passthrough
|
||||
SUBCOMMAND="${SUBCOMMAND:-$1}"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Validate subcommand ────────────────────────────────────────────────────
|
||||
if [[ -z "$SUBCOMMAND" ]]; then
|
||||
log_error "No subcommand specified."
|
||||
usage
|
||||
fi
|
||||
|
||||
VALID_SUBCOMMANDS="status name channels square quickbooks vagaro all"
|
||||
valid=0
|
||||
for s in $VALID_SUBCOMMANDS; do
|
||||
if [[ "$s" == "$SUBCOMMAND" ]]; then
|
||||
valid=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $valid -eq 0 ]]; then
|
||||
log_error "Unknown subcommand: $SUBCOMMAND"
|
||||
log_error "Valid subcommands: $VALID_SUBCOMMANDS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Load .env (best-effort; warn if missing) ───────────────────────────────
|
||||
if ! load_env 2>/dev/null; then
|
||||
log_warn "Could not load .env — connect scripts may use defaults."
|
||||
fi
|
||||
|
||||
# ── Dispatch ───────────────────────────────────────────────────────────────
|
||||
log_section "S7: Connect — $SUBCOMMAND"
|
||||
|
||||
case "$SUBCOMMAND" in
|
||||
status)
|
||||
print_status
|
||||
;;
|
||||
name)
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
bash "$SCRIPT_DIR/connect/connect-name.sh" --apply
|
||||
else
|
||||
bash "$SCRIPT_DIR/connect/connect-name.sh"
|
||||
fi
|
||||
;;
|
||||
channels)
|
||||
channels_args=()
|
||||
[[ $APPLY -eq 1 ]] && channels_args+=(--apply)
|
||||
[[ -n "$TARGET" ]] && channels_args+=(--target "$TARGET")
|
||||
bash "$SCRIPT_DIR/connect/connect-channels.sh" "${channels_args[@]}"
|
||||
;;
|
||||
square)
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
bash "$SCRIPT_DIR/connect/connect-square.sh" --apply
|
||||
else
|
||||
bash "$SCRIPT_DIR/connect/connect-square.sh"
|
||||
fi
|
||||
;;
|
||||
quickbooks)
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
bash "$SCRIPT_DIR/connect/connect-quickbooks.sh" --apply
|
||||
else
|
||||
bash "$SCRIPT_DIR/connect/connect-quickbooks.sh"
|
||||
fi
|
||||
;;
|
||||
vagaro)
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
bash "$SCRIPT_DIR/connect/connect-vagaro.sh" --apply
|
||||
else
|
||||
bash "$SCRIPT_DIR/connect/connect-vagaro.sh"
|
||||
fi
|
||||
;;
|
||||
all)
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
log_warn "--target is ignored with 'all' subcommand (all targets are walked)."
|
||||
fi
|
||||
log_info "Walking all targets..."
|
||||
all_args=()
|
||||
[[ $APPLY -eq 1 ]] && all_args+=(--apply)
|
||||
echo ""
|
||||
bash "$SCRIPT_DIR/connect/connect-name.sh" "${all_args[@]}"
|
||||
echo ""
|
||||
bash "$SCRIPT_DIR/connect/connect-channels.sh" "${all_args[@]}"
|
||||
echo ""
|
||||
bash "$SCRIPT_DIR/connect/connect-square.sh" "${all_args[@]}"
|
||||
echo ""
|
||||
bash "$SCRIPT_DIR/connect/connect-quickbooks.sh" "${all_args[@]}"
|
||||
echo ""
|
||||
bash "$SCRIPT_DIR/connect/connect-vagaro.sh" "${all_args[@]}"
|
||||
echo ""
|
||||
log_section "All targets complete"
|
||||
print_status
|
||||
;;
|
||||
esac
|
||||
|
||||
log_info "S7 connect ($SUBCOMMAND) complete."
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/connect/connect-channels.sh — S7: Channels connect helper
|
||||
#
|
||||
# Connects messaging channels (WhatsApp, Email, Telegram) via nemohermes.
|
||||
#
|
||||
# Platform-first: all mutations via nemohermes channels commands.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/connect/connect-channels.sh [--dry-run|--apply] [--target whatsapp|email|telegram]
|
||||
# ./scripts/connect/connect-channels.sh --help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
# shellcheck source=../lib/env.sh
|
||||
source "$SCRIPT_DIR/../lib/env.sh"
|
||||
# shellcheck source=../lib/connect_state.sh
|
||||
source "$SCRIPT_DIR/../lib/connect_state.sh"
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
TARGET=""
|
||||
|
||||
# All available channels
|
||||
ALL_CHANNELS="whatsapp email telegram"
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Connect messaging channels via nemohermes.
|
||||
|
||||
Channels:
|
||||
whatsapp WhatsApp Business API channel
|
||||
email Email channel (thread-based interaction)
|
||||
telegram Telegram bot channel
|
||||
|
||||
Options:
|
||||
--dry-run Preview only (default)
|
||||
--apply Execute mutations
|
||||
--target <channel> Connect specific channel (default: all)
|
||||
--help Show this help
|
||||
|
||||
Safety:
|
||||
--dry-run is the default. Use --apply to perform mutations.
|
||||
Channel changes may require a sandbox rebuild per the NemoClaw runtime matrix.
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --dry-run # preview all channels
|
||||
$(basename "$0") --target whatsapp --apply # connect WhatsApp only
|
||||
$(basename "$0") --apply # connect all channels
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
DRY_RUN=0
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
--target)
|
||||
shift
|
||||
TARGET="${1:-}"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
log_error "--target requires a value (whatsapp|email|telegram)"
|
||||
exit 1
|
||||
fi
|
||||
# Validate target
|
||||
valid=0
|
||||
for ch in $ALL_CHANNELS; do
|
||||
if [[ "$ch" == "$TARGET" ]]; then
|
||||
valid=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $valid -eq 0 ]]; then
|
||||
log_error "Invalid channel: $TARGET"
|
||||
log_error "Valid channels: $ALL_CHANNELS"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Load .env ──────────────────────────────────────────────────────────────
|
||||
load_env
|
||||
|
||||
SANDBOX_NAME="$(get_sandbox_name)"
|
||||
|
||||
log_section "Channels Connect"
|
||||
|
||||
# ── Check prerequisites ────────────────────────────────────────────────────
|
||||
require_cmd nemohermes "Install nemohermes CLI (part of NemoClaw platform)"
|
||||
|
||||
if ! nemohermes "$SANDBOX_NAME" status &>/dev/null 2>&1; then
|
||||
log_error "Sandbox '$SANDBOX_NAME' not found. Run S4 first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Determine which channels to process ────────────────────────────────────
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
CHANNELS_TO_PROCESS="$TARGET"
|
||||
else
|
||||
CHANNELS_TO_PROCESS="$ALL_CHANNELS"
|
||||
fi
|
||||
|
||||
# ── Channel connect functions ──────────────────────────────────────────────
|
||||
connect_whatsapp() {
|
||||
log_section "WhatsApp Channel"
|
||||
|
||||
local current_status
|
||||
current_status="$(get_status "whatsapp")"
|
||||
log_info "Current status: ${current_status:-not configured}"
|
||||
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log_info "[DRY-RUN] Would configure WhatsApp channel."
|
||||
log_info "Steps:"
|
||||
log_info " 1. Owner completes WhatsApp Business API setup in Meta developer console"
|
||||
log_info " 2. Operator runs: nemohermes $SANDBOX_NAME channels add whatsapp --phone-number-id <ID> --verify-token <TOKEN>"
|
||||
log_info " 3. Sandbox rebuild may be required per runtime matrix"
|
||||
log_info ""
|
||||
log_info "To execute: $(basename "$0") --target whatsapp --apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Apply mode: prompt for credentials
|
||||
log_info "WhatsApp channel configuration:"
|
||||
log_info "The owner must first set up WhatsApp Business API in the Meta developer console."
|
||||
log_info "You will need the Phone Number ID and a Verify Token."
|
||||
log_info ""
|
||||
|
||||
local phone_number_id=""
|
||||
local verify_token=""
|
||||
|
||||
read -rp "Phone Number ID: " phone_number_id
|
||||
if [[ -z "$phone_number_id" ]]; then
|
||||
log_warn "No Phone Number ID provided. Skipping WhatsApp."
|
||||
set_status "whatsapp" "skipped" "Operator skipped — no Phone Number ID"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Read verify token silently
|
||||
read -rsp "Verify Token: " verify_token
|
||||
echo ""
|
||||
if [[ -z "$verify_token" ]]; then
|
||||
log_warn "No Verify Token provided. Skipping WhatsApp."
|
||||
set_status "whatsapp" "skipped" "Operator skipped — no Verify Token"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Adding WhatsApp channel…"
|
||||
# NOTE: Verify token passed as CLI arg is briefly visible in /proc/*/cmdline.
|
||||
# This is a platform limitation of nemohermes which does not yet support
|
||||
# --from-stdin for channel credentials.
|
||||
if nemohermes "$SANDBOX_NAME" channels add whatsapp \
|
||||
--phone-number-id "$phone_number_id" \
|
||||
--verify-token "$verify_token" 2>&1; then
|
||||
log_info "WhatsApp channel added successfully."
|
||||
set_status "whatsapp" "connected" "WhatsApp channel active"
|
||||
else
|
||||
log_warn "WhatsApp channel add failed (may need rebuild)."
|
||||
set_status "whatsapp" "error" "WhatsApp channel add failed — check nemohermes output"
|
||||
fi
|
||||
}
|
||||
|
||||
connect_email() {
|
||||
log_section "Email Channel"
|
||||
|
||||
local current_status
|
||||
current_status="$(get_status "email")"
|
||||
log_info "Current status: ${current_status:-not configured}"
|
||||
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log_info "[DRY-RUN] Would configure Email channel."
|
||||
log_info "Steps:"
|
||||
log_info " 1. Owner provides email address for the assistant"
|
||||
log_info " 2. Operator runs: nemohermes $SANDBOX_NAME channels add email --address <EMAIL>"
|
||||
log_info " 3. Sandbox rebuild may be required per runtime matrix"
|
||||
log_info ""
|
||||
log_info "To execute: $(basename "$0") --target email --apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Email channel configuration:"
|
||||
log_info "The owner provides the email address the assistant will respond to."
|
||||
log_info ""
|
||||
|
||||
local email_address=""
|
||||
read -rp "Email address for assistant: " email_address
|
||||
if [[ -z "$email_address" ]]; then
|
||||
log_warn "No email address provided. Skipping Email."
|
||||
set_status "email" "skipped" "Operator skipped — no email address"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Adding Email channel…"
|
||||
if nemohermes "$SANDBOX_NAME" channels add email \
|
||||
--address "$email_address" 2>&1; then
|
||||
log_info "Email channel added successfully."
|
||||
set_status "email" "connected" "Email channel active"
|
||||
else
|
||||
log_warn "Email channel add failed (may need rebuild)."
|
||||
set_status "email" "error" "Email channel add failed — check nemohermes output"
|
||||
fi
|
||||
}
|
||||
|
||||
connect_telegram() {
|
||||
log_section "Telegram Channel"
|
||||
|
||||
local current_status
|
||||
current_status="$(get_status "telegram")"
|
||||
log_info "Current status: ${current_status:-not configured}"
|
||||
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log_info "[DRY-RUN] Would configure Telegram channel."
|
||||
log_info "Steps:"
|
||||
log_info " 1. Owner creates a bot via @BotFather on Telegram"
|
||||
log_info " 2. BotFather returns a Bot Token"
|
||||
log_info " 3. Operator runs: nemohermes $SANDBOX_NAME channels add telegram --bot-token <TOKEN>"
|
||||
log_info " 4. Sandbox rebuild may be required per runtime matrix"
|
||||
log_info ""
|
||||
log_info "To execute: $(basename "$0") --target telegram --apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Telegram channel configuration:"
|
||||
log_info "The owner must first create a bot via @BotFather on Telegram."
|
||||
log_info "BotFather returns a Bot Token."
|
||||
log_info ""
|
||||
|
||||
local bot_token=""
|
||||
read -rsp "Telegram Bot Token: " bot_token
|
||||
echo ""
|
||||
if [[ -z "$bot_token" ]]; then
|
||||
log_warn "No Bot Token provided. Skipping Telegram."
|
||||
set_status "telegram" "skipped" "Operator skipped — no Bot Token"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Adding Telegram channel…"
|
||||
# NOTE: Bot token passed as CLI arg is briefly visible in /proc/*/cmdline.
|
||||
# This is a platform limitation of nemohermes which does not yet support
|
||||
# --from-stdin for channel credentials.
|
||||
if nemohermes "$SANDBOX_NAME" channels add telegram \
|
||||
--bot-token "$bot_token" 2>&1; then
|
||||
log_info "Telegram channel added successfully."
|
||||
set_status "telegram" "connected" "Telegram channel active"
|
||||
else
|
||||
log_warn "Telegram channel add failed (may need rebuild)."
|
||||
set_status "telegram" "error" "Telegram channel add failed — check nemohermes output"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Execute ────────────────────────────────────────────────────────────────
|
||||
for channel in $CHANNELS_TO_PROCESS; do
|
||||
case "$channel" in
|
||||
whatsapp) connect_whatsapp ;;
|
||||
email) connect_email ;;
|
||||
telegram) connect_telegram ;;
|
||||
*)
|
||||
log_error "Unknown channel: $channel"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log_info "Channels connect complete."
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/connect/connect-name.sh — S7: Name / profile connect helper
|
||||
#
|
||||
# Guides the operator through naming the assistant and setting the profile.
|
||||
# The assistant name becomes the NemoClaw sandbox display identity.
|
||||
#
|
||||
# Platform-first: uses nemohermes for sandbox operations.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/connect/connect-name.sh [--dry-run|--apply]
|
||||
# ./scripts/connect/connect-name.sh --help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
# shellcheck source=../lib/env.sh
|
||||
source "$SCRIPT_DIR/../lib/env.sh"
|
||||
# shellcheck source=../lib/connect_state.sh
|
||||
source "$SCRIPT_DIR/../lib/connect_state.sh"
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Name / profile connect helper.
|
||||
|
||||
Sets the assistant name (sandbox display identity) and verifies the profile
|
||||
is configured. The assistant name is stored in LUMINA_SANDBOX in .env.
|
||||
|
||||
Options:
|
||||
--dry-run Preview only (default)
|
||||
--apply Execute mutations
|
||||
--help Show this help
|
||||
|
||||
Safety:
|
||||
--dry-run is the default. Use --apply to perform mutations.
|
||||
Renaming an existing sandbox may require a rebuild.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
DRY_RUN=0
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Load .env ──────────────────────────────────────────────────────────────
|
||||
load_env
|
||||
|
||||
SANDBOX_NAME="$(get_sandbox_name)"
|
||||
|
||||
log_section "Name / Profile Setup"
|
||||
|
||||
# ── Current state ──────────────────────────────────────────────────────────
|
||||
current_status="$(get_status "name")"
|
||||
|
||||
if [[ -n "$current_status" ]]; then
|
||||
log_info "Current name status: $current_status"
|
||||
fi
|
||||
|
||||
log_info "Current sandbox name: $SANDBOX_NAME"
|
||||
|
||||
# ── Check sandbox exists ───────────────────────────────────────────────────
|
||||
if nemohermes_available; then
|
||||
if nemohermes "$SANDBOX_NAME" status &>/dev/null 2>&1; then
|
||||
log_info "Sandbox '$SANDBOX_NAME' is reachable."
|
||||
else
|
||||
log_warn "Sandbox '$SANDBOX_NAME' not found. Run S4 (sandbox onboard) first."
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
set_status "name" "error" "Sandbox not found — run S4 first"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_warn "nemohermes not available — cannot verify sandbox"
|
||||
fi
|
||||
|
||||
# ── Name guidance ──────────────────────────────────────────────────────────
|
||||
log_info ""
|
||||
log_info "The assistant name is the display identity the owner sees."
|
||||
log_info "It is stored as LUMINA_SANDBOX in .env and used by nemohermes."
|
||||
log_info ""
|
||||
log_info "Platform naming rules:"
|
||||
log_info " - Lowercase letters, digits, hyphens, underscores"
|
||||
log_info " - 1-63 characters"
|
||||
log_info " - Must be unique within the NemoClaw registry"
|
||||
log_info ""
|
||||
|
||||
# ── Rename (if --apply and operator wants to change) ───────────────────────
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
log_info "Current name: $SANDBOX_NAME"
|
||||
log_info ""
|
||||
log_info "To rename the assistant:"
|
||||
log_info " 1. Edit LUMINA_SANDBOX in .env"
|
||||
log_info " 2. Run: nemohermes <new-name> onboard (creates new sandbox)"
|
||||
log_info " 3. Re-run S5 (policy + skills) for the new sandbox"
|
||||
log_info ""
|
||||
log_warn "Renaming requires creating a new sandbox. The old sandbox is NOT"
|
||||
log_warn "automatically deleted. Delete it manually if no longer needed."
|
||||
log_info ""
|
||||
|
||||
# For now, we record the name as connected if sandbox exists
|
||||
set_status "name" "connected" "Sandbox '$SANDBOX_NAME' verified"
|
||||
log_info "Name status recorded as connected."
|
||||
else
|
||||
log_info "[DRY-RUN] Would verify sandbox name and record status."
|
||||
log_info "Use --apply to record the name status."
|
||||
fi
|
||||
|
||||
# ── Profile guidance ───────────────────────────────────────────────────────
|
||||
log_info ""
|
||||
log_info "Profile intake (business name, timezone, hours, priorities, hard rules)"
|
||||
log_info "is handled through the setup-education skill in the assistant chat."
|
||||
log_info "The operator does not need to configure this via scripts."
|
||||
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
# Check if profile status is already set
|
||||
profile_status="$(get_status "profile")"
|
||||
if [[ -z "$profile_status" ]]; then
|
||||
# Default: profile is handled by owner in chat, not operator script
|
||||
set_status "profile" "later" "Profile intake via setup-education skill in chat"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Name / profile connect complete."
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/connect/connect-quickbooks.sh — S7: QuickBooks Online connect helper
|
||||
#
|
||||
# Connects QuickBooks Online via local MCP (intuit/quickbooks-online-mcp-server).
|
||||
# The MCP server runs as a container on the same Docker network as Hermes.
|
||||
#
|
||||
# Allows: reports, search/get invoices, bills, vendors, customers, company info.
|
||||
# Denies: create_payment, bill_payment, money movement; write/update/delete off for MVP.
|
||||
#
|
||||
# Platform-first: uses nemohermes config set for MCP registration.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/connect/connect-quickbooks.sh [--dry-run|--apply]
|
||||
# ./scripts/connect/connect-quickbooks.sh --help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
# shellcheck source=../lib/env.sh
|
||||
source "$SCRIPT_DIR/../lib/env.sh"
|
||||
# shellcheck source=../lib/connect_state.sh
|
||||
source "$SCRIPT_DIR/../lib/connect_state.sh"
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
|
||||
# QBO MCP configuration
|
||||
QBO_MCP_IMAGE="ghcr.io/intuit/quickbooks-online-mcp-server:latest"
|
||||
QBO_MCP_CONTAINER="lumina-qbo-mcp"
|
||||
QBO_MCP_NETWORK="lumina-network"
|
||||
|
||||
# Allowed tools (read-only)
|
||||
QBO_ALLOWED_TOOLS=(
|
||||
"get_report"
|
||||
"search_invoice"
|
||||
"get_invoice"
|
||||
"search_bill"
|
||||
"get_bill"
|
||||
"search_vendor"
|
||||
"get_vendor"
|
||||
"search_customer"
|
||||
"get_customer"
|
||||
"get_company_info"
|
||||
)
|
||||
|
||||
# Denied tools
|
||||
QBO_DENIED_TOOLS=(
|
||||
"create_payment"
|
||||
"bill_payment"
|
||||
"create_invoice"
|
||||
"update_invoice"
|
||||
"delete_invoice"
|
||||
"create_bill"
|
||||
"update_bill"
|
||||
"delete_bill"
|
||||
)
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Connect QuickBooks Online via local MCP.
|
||||
|
||||
QBO MCP:
|
||||
Image: $QBO_MCP_IMAGE
|
||||
Container: $QBO_MCP_CONTAINER
|
||||
Network: $QBO_MCP_NETWORK (same Docker network as Hermes)
|
||||
Type: Local MCP (stdio or network-attached)
|
||||
|
||||
Allowed tools (read-only):
|
||||
reports, search/get invoices, bills, vendors, customers, company info
|
||||
|
||||
Denied tools:
|
||||
create_payment, bill_payment, write/update/delete operations
|
||||
|
||||
Options:
|
||||
--dry-run Preview only (default)
|
||||
--apply Execute mutations (prompts for QBO credentials)
|
||||
--help Show this help
|
||||
|
||||
Safety:
|
||||
--dry-run is the default. Use --apply to perform mutations.
|
||||
QBO credentials are stored via OpenShell provider store — never in .env.
|
||||
Payment tools are explicitly denied in the MCP tool filter.
|
||||
Write/update/delete operations are disabled for MVP.
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --dry-run
|
||||
$(basename "$0") --apply
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
DRY_RUN=0
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Load .env ──────────────────────────────────────────────────────────────
|
||||
load_env
|
||||
|
||||
SANDBOX_NAME="$(get_sandbox_name)"
|
||||
|
||||
log_section "QuickBooks Online Connect"
|
||||
|
||||
# ── Current state ──────────────────────────────────────────────────────────
|
||||
current_status="$(get_status "quickbooks")"
|
||||
if [[ -n "$current_status" ]]; then
|
||||
log_info "Current status: $current_status"
|
||||
fi
|
||||
|
||||
# ── Check prerequisites ────────────────────────────────────────────────────
|
||||
require_cmd nemohermes "Install nemohermes CLI (part of NemoClaw platform)"
|
||||
require_cmd docker "Docker required for local MCP container"
|
||||
|
||||
if ! nemohermes "$SANDBOX_NAME" status &>/dev/null 2>&1; then
|
||||
log_error "Sandbox '$SANDBOX_NAME' not found. Run S4 first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Dry-run mode ───────────────────────────────────────────────────────────
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log_info "[DRY-RUN] QuickBooks Online connect preview:"
|
||||
log_info ""
|
||||
log_info " MCP Image: $QBO_MCP_IMAGE"
|
||||
log_info " Container: $QBO_MCP_CONTAINER"
|
||||
log_info " Network: $QBO_MCP_NETWORK"
|
||||
log_info " Sandbox: $SANDBOX_NAME"
|
||||
log_info ""
|
||||
log_info " Allowed tools (read-only):"
|
||||
for tool in "${QBO_ALLOWED_TOOLS[@]}"; do
|
||||
log_info " - $tool"
|
||||
done
|
||||
log_info ""
|
||||
log_info " Denied tools:"
|
||||
for tool in "${QBO_DENIED_TOOLS[@]}"; do
|
||||
log_info " - $tool"
|
||||
done
|
||||
log_info ""
|
||||
log_info " Steps to connect:"
|
||||
log_info " 1. Owner creates QBO app at developer.intuit.com"
|
||||
log_info " 2. Owner completes OAuth flow to get access token"
|
||||
log_info " 3. Operator stores credentials: openshell provider set qbo-* <VALUES>"
|
||||
log_info " 4. Operator starts MCP container on Docker network"
|
||||
log_info " 5. Operator registers MCP: nemohermes $SANDBOX_NAME config set mcp_servers.quickbooks"
|
||||
log_info " 6. Tool allowlist/denylist applied via nemohermes config"
|
||||
log_info ""
|
||||
log_info " To execute: $(basename "$0") --apply"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Apply mode ─────────────────────────────────────────────────────────────
|
||||
log_info "QuickBooks Online connect (apply mode):"
|
||||
log_info ""
|
||||
log_info "The owner must first:"
|
||||
log_info " 1. Create a QBO application at developer.intuit.com"
|
||||
log_info " 2. Complete OAuth flow to obtain Client ID, Client Secret, and Access Token"
|
||||
log_info " 3. Provide credentials to the operator"
|
||||
log_info ""
|
||||
|
||||
# Store credentials via OpenShell provider store
|
||||
if openshell_available; then
|
||||
log_info "Storing QBO credentials in OpenShell provider store…"
|
||||
|
||||
local_client_id=""
|
||||
local_client_secret=""
|
||||
local_access_token=""
|
||||
local_realm_id=""
|
||||
|
||||
read -rp "QBO Client ID: " local_client_id
|
||||
read -rsp "QBO Client Secret: " local_client_secret
|
||||
echo ""
|
||||
read -rsp "QBO Access Token: " local_access_token
|
||||
echo ""
|
||||
read -rp "QBO Realm ID (company ID): " local_realm_id
|
||||
|
||||
if [[ -z "$local_client_id" || -z "$local_client_secret" || -z "$local_access_token" ]]; then
|
||||
log_warn "Incomplete QBO credentials. Skipping."
|
||||
set_status "quickbooks" "skipped" "Operator skipped — incomplete credentials"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Store each credential (never echo them). Track failures.
|
||||
# NOTE: Credentials passed as CLI args to openshell are briefly visible in
|
||||
# /proc/*/cmdline and ps output. This is a platform limitation of openshell
|
||||
# which does not yet support --from-stdin for provider values.
|
||||
store_fail=0
|
||||
if openshell provider set qbo-client-id "$local_client_id" 2>&1; then
|
||||
log_info " qbo-client-id stored."
|
||||
else
|
||||
log_warn " qbo-client-id store failed — check openshell provider store."
|
||||
store_fail=1
|
||||
fi
|
||||
if openshell provider set qbo-client-secret "$local_client_secret" 2>&1; then
|
||||
log_info " qbo-client-secret stored."
|
||||
else
|
||||
log_warn " qbo-client-secret store failed — check openshell provider store."
|
||||
store_fail=1
|
||||
fi
|
||||
if openshell provider set qbo-access-token "$local_access_token" 2>&1; then
|
||||
log_info " qbo-access-token stored."
|
||||
else
|
||||
log_warn " qbo-access-token store failed — check openshell provider store."
|
||||
store_fail=1
|
||||
fi
|
||||
if [[ -n "$local_realm_id" ]]; then
|
||||
if openshell provider set qbo-realm-id "$local_realm_id" 2>&1; then
|
||||
log_info " qbo-realm-id stored."
|
||||
else
|
||||
log_warn " qbo-realm-id store failed — check openshell provider store."
|
||||
store_fail=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $store_fail -eq 1 ]]; then
|
||||
log_warn "Some credentials failed to store. Verify with: openshell provider list"
|
||||
else
|
||||
log_info "QBO credentials stored in provider store."
|
||||
fi
|
||||
else
|
||||
log_warn "openshell not available. Cannot store credentials in provider store."
|
||||
log_warn "Store manually: openshell provider set qbo-* <VALUES>"
|
||||
fi
|
||||
|
||||
# Check if MCP container already exists
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${QBO_MCP_CONTAINER}$"; then
|
||||
log_info "QBO MCP container '$QBO_MCP_CONTAINER' already exists."
|
||||
log_info "To recreate: docker rm -f $QBO_MCP_CONTAINER"
|
||||
else
|
||||
log_info "QBO MCP container not yet created."
|
||||
log_info "When ready, start with:"
|
||||
log_info " docker run -d --name $QBO_MCP_CONTAINER \\"
|
||||
log_info " --network $QBO_MCP_NETWORK \\"
|
||||
log_info " -e QB_CLIENT_ID=<CLIENT_ID> \\"
|
||||
log_info " -e QB_CLIENT_SECRET=<CLIENT_SECRET> \\"
|
||||
log_info " -e QB_ACCESS_TOKEN=<ACCESS_TOKEN> \\"
|
||||
log_info " -e QB_REALM_ID=<REALM_ID> \\"
|
||||
log_info " $QBO_MCP_IMAGE"
|
||||
log_info ""
|
||||
log_info "Or add to deploy/compose/docker-compose.yml for managed lifecycle."
|
||||
fi
|
||||
|
||||
# Register MCP server in Hermes config
|
||||
log_info "Registering QBO MCP server in Hermes config…"
|
||||
if nemohermes "$SANDBOX_NAME" config set \
|
||||
mcp_servers.quickbooks.type "local" 2>&1; then
|
||||
log_info "QBO MCP type registered."
|
||||
else
|
||||
log_warn "MCP config registration may need manual setup."
|
||||
log_warn "Manual: nemohermes $SANDBOX_NAME config set mcp_servers.quickbooks"
|
||||
fi
|
||||
|
||||
# Apply tool allowlist
|
||||
log_info "Applying QBO tool allowlist (read-only)…"
|
||||
log_info " Allowed: ${QBO_ALLOWED_TOOLS[*]}"
|
||||
log_info " Denied: ${QBO_DENIED_TOOLS[*]}"
|
||||
|
||||
# Note: The actual tool filtering is done via mcp_servers config with
|
||||
# tools.include / tools.exclude. The exact nemohermes subcommand varies
|
||||
# by platform version. This is deferred until the platform CLI supports
|
||||
# per-server tool filtering in a stable form.
|
||||
log_info "[DEFERRED] Tool filters will be applied via nemohermes config set"
|
||||
log_info " when the platform CLI supports per-server tools.include/exclude."
|
||||
|
||||
set_status "quickbooks" "connected" "QBO local MCP registered (read-only tools)"
|
||||
log_info "QuickBooks Online connect complete."
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/connect/connect-square.sh — S7: Square connect helper
|
||||
#
|
||||
# Connects Square via remote MCP (mcp.squareup.com).
|
||||
# Allows: bookings, customers, catalog, inventory/location reads.
|
||||
# Denies: payments, refunds, cards, checkout, payouts.
|
||||
#
|
||||
# Platform-first: uses nemohermes config set for MCP registration.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/connect/connect-square.sh [--dry-run|--apply]
|
||||
# ./scripts/connect/connect-square.sh --help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
# shellcheck source=../lib/env.sh
|
||||
source "$SCRIPT_DIR/../lib/env.sh"
|
||||
# shellcheck source=../lib/connect_state.sh
|
||||
source "$SCRIPT_DIR/../lib/connect_state.sh"
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
|
||||
# Square MCP configuration
|
||||
SQUARE_MCP_URL="https://mcp.squareup.com/v1"
|
||||
|
||||
# Allowed tools (read-only)
|
||||
SQUARE_ALLOWED_TOOLS=(
|
||||
"bookings/list_bookings"
|
||||
"bookings/get_booking"
|
||||
"customers/list_customers"
|
||||
"customers/get_customer"
|
||||
"catalog/list_catalog"
|
||||
"catalog/search_catalog_objects"
|
||||
"inventory/list_inventory"
|
||||
"locations/list_locations"
|
||||
"locations/get_location"
|
||||
)
|
||||
|
||||
# Denied tools (payment-related)
|
||||
SQUARE_DENIED_TOOLS=(
|
||||
"payments/*"
|
||||
"refunds/*"
|
||||
"cards/*"
|
||||
"checkout/*"
|
||||
"payouts/*"
|
||||
)
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Connect Square via remote MCP.
|
||||
|
||||
Square MCP:
|
||||
URL: $SQUARE_MCP_URL
|
||||
Type: Remote MCP (HTTP/SSE)
|
||||
|
||||
Allowed tools (read-only):
|
||||
bookings, customers, catalog, inventory, locations
|
||||
|
||||
Denied tools:
|
||||
payments, refunds, cards, checkout, payouts
|
||||
|
||||
Options:
|
||||
--dry-run Preview only (default)
|
||||
--apply Execute mutations (prompts for Square access token)
|
||||
--help Show this help
|
||||
|
||||
Safety:
|
||||
--dry-run is the default. Use --apply to perform mutations.
|
||||
The Square access token is stored via OpenShell provider store — never in .env.
|
||||
Payment tools are explicitly denied in the MCP tool filter.
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --dry-run
|
||||
$(basename "$0") --apply
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
DRY_RUN=0
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Load .env ──────────────────────────────────────────────────────────────
|
||||
load_env
|
||||
|
||||
SANDBOX_NAME="$(get_sandbox_name)"
|
||||
|
||||
log_section "Square Connect"
|
||||
|
||||
# ── Current state ──────────────────────────────────────────────────────────
|
||||
current_status="$(get_status "square")"
|
||||
if [[ -n "$current_status" ]]; then
|
||||
log_info "Current status: $current_status"
|
||||
fi
|
||||
|
||||
# ── Check prerequisites ────────────────────────────────────────────────────
|
||||
require_cmd nemohermes "Install nemohermes CLI (part of NemoClaw platform)"
|
||||
|
||||
if ! nemohermes "$SANDBOX_NAME" status &>/dev/null 2>&1; then
|
||||
log_error "Sandbox '$SANDBOX_NAME' not found. Run S4 first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Dry-run mode ───────────────────────────────────────────────────────────
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log_info "[DRY-RUN] Square connect preview:"
|
||||
log_info ""
|
||||
log_info " MCP URL: $SQUARE_MCP_URL"
|
||||
log_info " Type: Remote MCP"
|
||||
log_info " Sandbox: $SANDBOX_NAME"
|
||||
log_info ""
|
||||
log_info " Allowed tools (read-only):"
|
||||
for tool in "${SQUARE_ALLOWED_TOOLS[@]}"; do
|
||||
log_info " - $tool"
|
||||
done
|
||||
log_info ""
|
||||
log_info " Denied tools:"
|
||||
for tool in "${SQUARE_DENIED_TOOLS[@]}"; do
|
||||
log_info " - $tool"
|
||||
done
|
||||
log_info ""
|
||||
log_info " Steps to connect:"
|
||||
log_info " 1. Owner creates Square application at developer.squareup.com"
|
||||
log_info " 2. Owner generates an OAuth access token (read scope)"
|
||||
log_info " 3. Operator stores token: openshell provider set square-access-token <TOKEN>"
|
||||
log_info " 4. Operator registers MCP: nemohermes $SANDBOX_NAME config set mcp_servers.square.url $SQUARE_MCP_URL"
|
||||
log_info " 5. Tool allowlist/denylist applied via nemohermes config"
|
||||
log_info ""
|
||||
log_info " To execute: $(basename "$0") --apply"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Apply mode ─────────────────────────────────────────────────────────────
|
||||
log_info "Square connect (apply mode):"
|
||||
log_info ""
|
||||
log_info "The owner must first:"
|
||||
log_info " 1. Create a Square application at developer.squareup.com"
|
||||
log_info " 2. Generate an OAuth access token with read-only scope"
|
||||
log_info " 3. Provide the token to the operator"
|
||||
log_info ""
|
||||
|
||||
# Store token via OpenShell provider store
|
||||
if openshell_available; then
|
||||
log_info "Storing Square access token in OpenShell provider store…"
|
||||
local_token=""
|
||||
read -rsp "Square Access Token: " local_token
|
||||
echo ""
|
||||
if [[ -z "$local_token" ]]; then
|
||||
log_warn "No access token provided. Skipping Square."
|
||||
set_status "square" "skipped" "Operator skipped — no access token"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Store via openshell (never echo the token).
|
||||
# NOTE: Credentials passed as CLI args to openshell are briefly visible in
|
||||
# /proc/*/cmdline and ps output. This is a platform limitation of openshell
|
||||
# which does not yet support --from-stdin for provider values.
|
||||
if openshell provider set square-access-token "$local_token" 2>&1; then
|
||||
log_info "Square access token stored in provider store."
|
||||
else
|
||||
log_warn "Could not store token via openshell. Token may need manual setup."
|
||||
log_warn "Run: openshell provider set square-access-token <TOKEN>"
|
||||
fi
|
||||
else
|
||||
log_warn "openshell not available. Cannot store token in provider store."
|
||||
log_warn "Store manually: openshell provider set square-access-token <TOKEN>"
|
||||
fi
|
||||
|
||||
# Register MCP server
|
||||
log_info "Registering Square MCP server…"
|
||||
if nemohermes "$SANDBOX_NAME" config set \
|
||||
mcp_servers.square.url "$SQUARE_MCP_URL" 2>&1; then
|
||||
log_info "Square MCP URL registered."
|
||||
else
|
||||
log_warn "MCP URL registration failed (may need different config path)."
|
||||
log_warn "Manual: nemohermes $SANDBOX_NAME config set mcp_servers.square.url $SQUARE_MCP_URL"
|
||||
fi
|
||||
|
||||
# Apply tool allowlist
|
||||
log_info "Applying Square tool allowlist (read-only)…"
|
||||
log_info " Allowed: ${SQUARE_ALLOWED_TOOLS[*]}"
|
||||
log_info " Denied: ${SQUARE_DENIED_TOOLS[*]}"
|
||||
|
||||
# Note: The actual tool filtering is done via mcp_servers config with
|
||||
# tools.include / tools.exclude. The exact nemohermes subcommand varies
|
||||
# by platform version. This is deferred until the platform CLI supports
|
||||
# per-server tool filtering in a stable form.
|
||||
log_info "[DEFERRED] Tool filters will be applied via nemohermes config set"
|
||||
log_info " when the platform CLI supports per-server tools.include/exclude."
|
||||
|
||||
set_status "square" "connected" "Square remote MCP registered (read-only tools)"
|
||||
log_info "Square connect complete."
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/connect/connect-vagaro.sh — S7: Vagaro connect helper
|
||||
#
|
||||
# Connects Vagaro via REST API + webhooks.
|
||||
# No public MCP exists for Vagaro — our services handle the integration.
|
||||
#
|
||||
# Allows: appointments, clients, services, staff.
|
||||
# Denies: no scrape, no unofficial access.
|
||||
#
|
||||
# Platform-first: credentials via OpenShell provider store.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/connect/connect-vagaro.sh [--dry-run|--apply]
|
||||
# ./scripts/connect/connect-vagaro.sh --help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Source shared helpers
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
# shellcheck source=../lib/env.sh
|
||||
source "$SCRIPT_DIR/../lib/env.sh"
|
||||
# shellcheck source=../lib/connect_state.sh
|
||||
source "$SCRIPT_DIR/../lib/connect_state.sh"
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
|
||||
# Vagaro API configuration
|
||||
VAGARO_API_BASE="https://api.vagaro.com"
|
||||
VAGARO_WEBHOOK_PORT="${LUMINA_VAGARO_WEBHOOK_PORT:-9876}"
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Connect Vagaro via REST API + webhooks.
|
||||
|
||||
Vagaro integration:
|
||||
API Base: $VAGARO_API_BASE
|
||||
Type: REST + webhooks (no public MCP)
|
||||
Webhook port: $VAGARO_WEBHOOK_PORT
|
||||
|
||||
Allowed operations:
|
||||
appointments, clients, services, staff
|
||||
|
||||
Denied:
|
||||
No scrape, no unofficial access
|
||||
|
||||
Options:
|
||||
--dry-run Preview only (default)
|
||||
--apply Execute mutations (prompts for Vagaro credentials)
|
||||
--help Show this help
|
||||
|
||||
Safety:
|
||||
--dry-run is the default. Use --apply to perform mutations.
|
||||
Vagaro credentials are stored via OpenShell provider store — never in .env.
|
||||
No unofficial scraping — only documented REST API.
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --dry-run
|
||||
$(basename "$0") --apply
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
APPLY=0
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
DRY_RUN=0
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Load .env ──────────────────────────────────────────────────────────────
|
||||
load_env
|
||||
|
||||
SANDBOX_NAME="$(get_sandbox_name)"
|
||||
|
||||
log_section "Vagaro Connect"
|
||||
|
||||
# ── Current state ──────────────────────────────────────────────────────────
|
||||
current_status="$(get_status "vagaro")"
|
||||
if [[ -n "$current_status" ]]; then
|
||||
log_info "Current status: $current_status"
|
||||
fi
|
||||
|
||||
# ── Check prerequisites ────────────────────────────────────────────────────
|
||||
require_cmd nemohermes "Install nemohermes CLI (part of NemoClaw platform)"
|
||||
|
||||
if ! nemohermes "$SANDBOX_NAME" status &>/dev/null 2>&1; then
|
||||
log_error "Sandbox '$SANDBOX_NAME' not found. Run S4 first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Dry-run mode ───────────────────────────────────────────────────────────
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log_info "[DRY-RUN] Vagaro connect preview:"
|
||||
log_info ""
|
||||
log_info " API Base: $VAGARO_API_BASE"
|
||||
log_info " Webhook: port $VAGARO_WEBHOOK_PORT"
|
||||
log_info " Sandbox: $SANDBOX_NAME"
|
||||
log_info ""
|
||||
log_info " Steps to connect:"
|
||||
log_info " 1. Owner creates Vagaro developer account at developer.vagaro.com"
|
||||
log_info " 2. Owner registers an application to get API credentials"
|
||||
log_info " 3. Owner configures webhook URL in Vagaro dashboard"
|
||||
log_info " 4. Operator stores credentials: openshell provider set vagaro-* <VALUES>"
|
||||
log_info " 5. Operator verifies API connectivity"
|
||||
log_info " 6. Webhook service started (compose or container)"
|
||||
log_info ""
|
||||
log_info " To execute: $(basename "$0") --apply"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Apply mode ─────────────────────────────────────────────────────────────
|
||||
log_info "Vagaro connect (apply mode):"
|
||||
log_info ""
|
||||
log_info "The owner must first:"
|
||||
log_info " 1. Create a Vagaro developer account at developer.vagaro.com"
|
||||
log_info " 2. Register an application to obtain API credentials"
|
||||
log_info " 3. Configure webhook URL in the Vagaro dashboard"
|
||||
log_info " 4. Provide credentials to the operator"
|
||||
log_info ""
|
||||
|
||||
# Store credentials via OpenShell provider store
|
||||
if ! openshell_available; then
|
||||
log_error "openshell not available. Cannot store credentials in provider store."
|
||||
log_error "Install openshell CLI, then re-run with --apply."
|
||||
set_status "vagaro" "error" "openshell not available — cannot store credentials"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Storing Vagaro credentials in OpenShell provider store…"
|
||||
|
||||
local_api_key=""
|
||||
local_api_secret=""
|
||||
local_webhook_secret=""
|
||||
|
||||
# SECURITY: API key silenced to prevent shoulder-surfing and terminal-log leakage.
|
||||
# NOTE: Credentials passed as CLI args to openshell are briefly visible in
|
||||
# /proc/*/cmdline and ps output. This is a platform limitation of openshell
|
||||
# which does not yet support --from-stdin for provider values.
|
||||
read -rsp "Vagaro API Key: " local_api_key
|
||||
echo ""
|
||||
read -rsp "Vagaro API Secret: " local_api_secret
|
||||
echo ""
|
||||
read -rsp "Vagaro Webhook Secret (optional): " local_webhook_secret
|
||||
echo ""
|
||||
|
||||
if [[ -z "$local_api_key" || -z "$local_api_secret" ]]; then
|
||||
log_warn "Incomplete Vagaro credentials. Skipping."
|
||||
set_status "vagaro" "skipped" "Operator skipped — incomplete credentials"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Store credentials (never echo them). Track failures.
|
||||
store_fail=0
|
||||
if openshell provider set vagaro-api-key "$local_api_key" 2>&1; then
|
||||
log_info " vagaro-api-key stored."
|
||||
else
|
||||
log_warn " vagaro-api-key store failed — check openshell provider store."
|
||||
store_fail=1
|
||||
fi
|
||||
if openshell provider set vagaro-api-secret "$local_api_secret" 2>&1; then
|
||||
log_info " vagaro-api-secret stored."
|
||||
else
|
||||
log_warn " vagaro-api-secret store failed — check openshell provider store."
|
||||
store_fail=1
|
||||
fi
|
||||
if [[ -n "$local_webhook_secret" ]]; then
|
||||
if openshell provider set vagaro-webhook-secret "$local_webhook_secret" 2>&1; then
|
||||
log_info " vagaro-webhook-secret stored."
|
||||
else
|
||||
log_warn " vagaro-webhook-secret store failed — check openshell provider store."
|
||||
store_fail=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $store_fail -eq 1 ]]; then
|
||||
log_warn "Some credentials failed to store. Verify with: openshell provider list"
|
||||
fi
|
||||
|
||||
# Verify API connectivity (best-effort)
|
||||
log_info "Verifying Vagaro API connectivity…"
|
||||
if curl -sf --max-time 10 \
|
||||
-H "X-Vagaro-API-Key: $local_api_key" \
|
||||
"${VAGARO_API_BASE}/v1/me" &>/dev/null; then
|
||||
log_info "Vagaro API connectivity verified."
|
||||
set_status "vagaro" "connected" "Vagaro REST API connected"
|
||||
else
|
||||
log_warn "Vagaro API verification failed (credentials may be incorrect or API unavailable)."
|
||||
log_warn "This may be non-fatal if the API requires specific scopes."
|
||||
set_status "vagaro" "error" "Vagaro API verification failed — check credentials"
|
||||
fi
|
||||
|
||||
# Webhook guidance
|
||||
log_info ""
|
||||
log_info "Webhook service:"
|
||||
log_info " The Vagaro webhook receiver runs on port $VAGARO_WEBHOOK_PORT."
|
||||
log_info " Configure the webhook URL in the Vagaro dashboard:"
|
||||
log_info " https://<your-host>:$VAGARO_WEBHOOK_PORT/vagaro/webhook"
|
||||
log_info " Or add to deploy/compose/docker-compose.yml for managed lifecycle."
|
||||
|
||||
log_info "Vagaro connect complete."
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/lib/connect_state.sh — S7: local capability state management
|
||||
# Sourced by connect scripts. Manages .local/capability_state.json.
|
||||
# Do not execute directly.
|
||||
#
|
||||
# All state is written to .local/ (gitignored) — never into repo-tracked files.
|
||||
# Fixtures under data/fixtures/setup/ remain untouched.
|
||||
#
|
||||
# SECURITY: All Python invocations pass data via stdin, env vars, or sys.argv.
|
||||
# Shell variables are NEVER interpolated into Python source strings.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Local state directory ──────────────────────────────────────────────────
|
||||
LOCAL_DIR="${REPO_ROOT}/.local"
|
||||
STATE_FILE="${LOCAL_DIR}/capability_state.json"
|
||||
|
||||
# Valid status values (must match setup-education ConnectionStatus enum)
|
||||
VALID_STATUSES="connected skipped later error offline"
|
||||
|
||||
# ── Ensure .local directory exists ─────────────────────────────────────────
|
||||
ensure_local_dir() {
|
||||
if [[ ! -d "$LOCAL_DIR" ]]; then
|
||||
mkdir -p "$LOCAL_DIR"
|
||||
log_info "Created local state directory: $LOCAL_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Read current state (returns empty JSON object if no state file) ────────
|
||||
read_state() {
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
cat "$STATE_FILE"
|
||||
else
|
||||
echo '{}'
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Get status for a target ────────────────────────────────────────────────
|
||||
# Usage: get_status "square" → "connected" | "skipped" | "later" | "error" | ""
|
||||
get_status() {
|
||||
local target="$1"
|
||||
local state
|
||||
state="$(read_state)"
|
||||
|
||||
# Pass state via stdin, target via env var — no string interpolation
|
||||
printf '%s' "$state" | TARGET="$target" python3 -c "
|
||||
import json, sys, os
|
||||
state = json.loads(sys.stdin.read())
|
||||
target = os.environ['TARGET']
|
||||
print(state.get(target, {}).get('status', ''))
|
||||
" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
# ── Set status for a target ────────────────────────────────────────────────
|
||||
# Usage: set_status "square" "connected" "Square MCP registered"
|
||||
set_status() {
|
||||
local target="$1"
|
||||
local status="$2"
|
||||
local details="${3:-}"
|
||||
|
||||
# Validate status
|
||||
local valid=0
|
||||
for s in $VALID_STATUSES; do
|
||||
if [[ "$s" == "$status" ]]; then
|
||||
valid=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $valid -eq 0 ]]; then
|
||||
log_error "Invalid status '$status'. Must be one of: $VALID_STATUSES"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ensure_local_dir
|
||||
|
||||
local state
|
||||
state="$(read_state)"
|
||||
|
||||
# Pass state via stdin, target/status/details via env vars — no interpolation
|
||||
printf '%s' "$state" | \
|
||||
CONNECT_TARGET="$target" \
|
||||
CONNECT_STATUS="$status" \
|
||||
CONNECT_DETAILS="$details" \
|
||||
python3 -c "
|
||||
import json, sys, os, datetime
|
||||
|
||||
state = json.loads(sys.stdin.read())
|
||||
target = os.environ['CONNECT_TARGET']
|
||||
status = os.environ['CONNECT_STATUS']
|
||||
details = os.environ['CONNECT_DETAILS']
|
||||
|
||||
state[target] = {
|
||||
'status': status,
|
||||
'details': details,
|
||||
'updated_at': datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
# Ensure top-level metadata
|
||||
if 'generated_at' not in state:
|
||||
state['generated_at'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
if 'is_fixture' not in state:
|
||||
state['is_fixture'] = False
|
||||
|
||||
json.dump(state, sys.stdout, indent=2)
|
||||
" > "${STATE_FILE}.tmp" 2>/dev/null
|
||||
|
||||
mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||
chmod 600 "$STATE_FILE"
|
||||
log_info "State updated: ${target} → ${status}"
|
||||
}
|
||||
|
||||
# ── Print status summary (human-readable) ──────────────────────────────────
|
||||
print_status() {
|
||||
local state
|
||||
state="$(read_state)"
|
||||
|
||||
if [[ "$state" == "{}" ]]; then
|
||||
log_info "No connection state recorded yet."
|
||||
log_info "Run connect commands to establish integrations."
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_section "Connection Status"
|
||||
|
||||
# Pass state via stdin — no interpolation
|
||||
printf '%s' "$state" | python3 -c "
|
||||
import json, sys
|
||||
|
||||
state = json.loads(sys.stdin.read())
|
||||
# Remove metadata keys
|
||||
meta_keys = {'generated_at', 'is_fixture'}
|
||||
targets = {k: v for k, v in state.items() if k not in meta_keys}
|
||||
|
||||
if not targets:
|
||||
print(' No targets configured yet.')
|
||||
else:
|
||||
status_icons = {
|
||||
'connected': '✅',
|
||||
'skipped': '⏭️',
|
||||
'later': '⏳',
|
||||
'error': '❌',
|
||||
'offline': '📋',
|
||||
}
|
||||
for target, info in sorted(targets.items()):
|
||||
status = info.get('status', 'unknown')
|
||||
icon = status_icons.get(status, '❓')
|
||||
details = info.get('details', '')
|
||||
updated = info.get('updated_at', '')
|
||||
display = target.replace('_', ' ').title()
|
||||
print(f' {icon} {display:25s} {status:10s} {details}')
|
||||
if updated:
|
||||
print(f' Updated: {updated}')
|
||||
" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Export state as capability report JSON (compatible with setup-education) ─
|
||||
# Produces the same schema as data/fixtures/setup/capability_matrix.json
|
||||
export_capability_report() {
|
||||
local output_file="${1:-}"
|
||||
local state
|
||||
state="$(read_state)"
|
||||
|
||||
if [[ "$state" == "{}" ]]; then
|
||||
log_warn "No connection state to export."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local sandbox_name
|
||||
sandbox_name="$(get_sandbox_name)"
|
||||
|
||||
# Pass state via stdin, sandbox_name via env var — no interpolation
|
||||
local report
|
||||
report="$(printf '%s' "$state" | \
|
||||
CONNECT_SANDBOX="$sandbox_name" \
|
||||
python3 -c "
|
||||
import json, sys, os, datetime
|
||||
|
||||
state = json.loads(sys.stdin.read())
|
||||
sandbox_name = os.environ['CONNECT_SANDBOX']
|
||||
meta_keys = {'generated_at', 'is_fixture'}
|
||||
targets = {k: v for k, v in state.items() if k not in meta_keys}
|
||||
|
||||
capabilities = []
|
||||
area_map = {
|
||||
'name': 'identity',
|
||||
'profile': 'profile',
|
||||
'whatsapp': 'channels',
|
||||
'email': 'channels',
|
||||
'telegram': 'channels',
|
||||
'square': 'scheduling',
|
||||
'quickbooks': 'books',
|
||||
'vagaro': 'scheduling',
|
||||
}
|
||||
|
||||
for target, info in sorted(targets.items()):
|
||||
area = area_map.get(target, 'other')
|
||||
capabilities.append({
|
||||
'area': area,
|
||||
'provider': target,
|
||||
'status': info.get('status', 'offline'),
|
||||
'details': info.get('details', ''),
|
||||
})
|
||||
|
||||
report = {
|
||||
'salon_name': sandbox_name,
|
||||
'is_fixture': False,
|
||||
'generated_at': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
'capabilities': capabilities,
|
||||
}
|
||||
|
||||
json.dump(report, sys.stdout, indent=2)
|
||||
" 2>/dev/null)"
|
||||
|
||||
if [[ -n "$output_file" ]]; then
|
||||
echo "$report" > "$output_file"
|
||||
log_info "Capability report exported to $output_file"
|
||||
else
|
||||
echo "$report"
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user