From 7a44fda5b4bbf2176c8c5584dda97cb21f4a0234 Mon Sep 17 00:00:00 2001 From: Daniel Sierra Date: Wed, 25 Mar 2026 16:37:51 +0000 Subject: [PATCH 1/6] feat: add selective reconfigure menu to install.sh When .env exists, show a two-level TUI menu letting users jump directly to any agent/integration setting instead of re-running the full sequential wizard. Co-Authored-By: Claude Sonnet 4.6 --- install.sh | 292 ++++++++++++++++++++++++++++++++++++++--- tests/reconfigure.bats | 181 +++++++++++++++++++++++++ 2 files changed, 453 insertions(+), 20 deletions(-) create mode 100644 tests/reconfigure.bats diff --git a/install.sh b/install.sh index dc83302..450cc18 100755 --- a/install.sh +++ b/install.sh @@ -26,6 +26,64 @@ if [ ! -f "$MANIFEST" ]; then exit 1 fi +# --- Helper: integration status --- +# _integration_status AGENT_ID INTEGRATION_KEY — returns "configured" or "not configured" +_integration_status() { + local agent_id="$1" + local intg="$2" + local agent_upper + agent_upper=$(echo "$agent_id" | tr '[:lower:]' '[:upper:]') + case "$intg" in + discord) + local token + token=$(env_get "DISCORD_${agent_upper}_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + github) + local token + token=$(env_get "GH_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + claude) + local token + token=$(env_get "CLAUDE_CODE_OAUTH_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + gws) + [ -f "$SCRIPT_DIR/home/.config/gws/credentials.json" ] && echo "configured" || echo "not configured" + ;; + xurl) + local token + token=$(env_get "X_BEARER_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + *) + echo "not configured" + ;; + esac +} + +# --- Helper: git identity prompts --- +_run_git_identity() { + print_header "Git Identity" + print_info "Used for commits made by Forge." + echo "" + local existing_name existing_email git_name git_email + existing_name=$(env_get "GIT_AUTHOR_NAME") + existing_email=$(env_get "GIT_AUTHOR_EMAIL") + git_name=$(gum_input "Full name" "${existing_name:-Your Name}") + if [ -z "$git_name" ] && [ -n "$existing_name" ]; then + git_name="$existing_name" + fi + git_email=$(gum_input "Email address" "${existing_email:-you@example.com}") + if [ -z "$git_email" ] && [ -n "$existing_email" ]; then + git_email="$existing_email" + fi + env_set "GIT_AUTHOR_NAME" "$git_name" + env_set "GIT_AUTHOR_EMAIL" "$git_email" + echo "" +} + # --- Detect mode --- RECONFIG=0 EXISTING_ENV="$SCRIPT_DIR/.env" @@ -64,6 +122,219 @@ if [ "$RECONFIG" -eq 1 ]; then fi print_info "You can add, remove, or update settings." echo "" + + # Source all integration modules for reconfigure mode + # shellcheck source=installer/discord.sh + source "$SCRIPT_DIR/installer/discord.sh" + # shellcheck source=installer/github.sh + source "$SCRIPT_DIR/installer/github.sh" + # shellcheck source=installer/claude.sh + source "$SCRIPT_DIR/installer/claude.sh" + # shellcheck source=installer/gws.sh + source "$SCRIPT_DIR/installer/gws.sh" + # shellcheck source=installer/xurl.sh + source "$SCRIPT_DIR/installer/xurl.sh" + + # --- Reconfigure: agent submenu --- + _run_agent_submenu() { + local agent_id="$1" + local agent_name="$2" + while true; do + local menu_items="" + # Git Identity first for Forge + if [ "$agent_id" = "forge" ]; then + local git_name + git_name=$(env_get "GIT_AUTHOR_NAME") + local git_status + [ -n "$git_name" ] && git_status="configured" || git_status="not configured" + menu_items="${menu_items}Git Identity (${git_status})\n" + fi + # Integrations from manifest + local intg_ids + intg_ids=$(jq -r --arg id "$agent_id" '.agents[] | select(.id == $id) | .integrations | keys[]' "$MANIFEST") + for intg in $intg_ids; do + local intg_label intg_status + intg_label=$(jq -r --arg intg "$intg" '.integrations[$intg].label' "$MANIFEST") + intg_status=$(_integration_status "$agent_id" "$intg") + menu_items="${menu_items}${intg_label} (${intg_status})\n" + done + menu_items="${menu_items}Back" + + print_header "$agent_name" + local choice + choice=$(printf "%b" "$menu_items" | gum choose --header "") + [ -z "$choice" ] && break + [ "$choice" = "Back" ] && break + + # Strip " (status)" suffix to get the label + local label + label=$(echo "$choice" | sed 's/ ([^)]*)$//') + case "$label" in + "Git Identity") + _run_git_identity + ;; + "Discord") + run_discord_shared + run_discord_agent "$agent_id" "$agent_name" + ;; + "GitHub") + run_github + ;; + "Claude Code") + run_claude + ;; + "Google Workspace") + run_gws + ;; + "X/Twitter") + run_xurl + ;; + esac + done + } + + # --- Reconfigure: top-level menu --- + run_reconfigure_menu() { + while true; do + local menu_items="" + local all_agent_ids + all_agent_ids=$(jq -r '.agents[].id' "$MANIFEST") + for aid in $all_agent_ids; do + local aid_upper aname token_val + aid_upper=$(echo "$aid" | tr '[:lower:]' '[:upper:]') + aname=$(jq -r --arg id "$aid" '.agents[] | select(.id == $id) | .name' "$MANIFEST") + token_val=$(env_get "DISCORD_${aid_upper}_TOKEN") + if [ -n "$token_val" ]; then + menu_items="${menu_items}${aname}\n" + else + menu_items="${menu_items}${aname} (not installed)\n" + fi + done + menu_items="${menu_items}Done" + + local choice + choice=$(printf "%b" "$menu_items" | gum choose --header "What would you like to configure?") + [ -z "$choice" ] && break + [ "$choice" = "Done" ] && break + + # Find agent id for chosen name + local chosen_id="" chosen_name="" + for aid in $all_agent_ids; do + local aname + aname=$(jq -r --arg id "$aid" '.agents[] | select(.id == $id) | .name' "$MANIFEST") + if [ "$choice" = "$aname" ] || [ "$choice" = "$aname (not installed)" ]; then + chosen_id="$aid" + chosen_name="$aname" + break + fi + done + [ -z "$chosen_id" ] && continue + + # Check if installed + local aid_upper token_val + aid_upper=$(echo "$chosen_id" | tr '[:lower:]' '[:upper:]') + token_val=$(env_get "DISCORD_${aid_upper}_TOKEN") + if [ -z "$token_val" ]; then + run_full_agent_setup "$chosen_id" + else + _run_agent_submenu "$chosen_id" "$chosen_name" + fi + done + } + + # --- Reconfigure: full agent setup (for not-installed agents) --- + run_full_agent_setup() { + local agent_id="$1" + local agent_name + agent_name=$(jq -r --arg id "$agent_id" '.agents[] | select(.id == $id) | .name' "$MANIFEST") + + print_header "Setting up $agent_name" + + # Git Identity for Forge + if [ "$agent_id" = "forge" ]; then + _run_git_identity + fi + + # Run each integration + local intg_ids + intg_ids=$(jq -r --arg id "$agent_id" '.agents[] | select(.id == $id) | .integrations | keys[]' "$MANIFEST") + for intg in $intg_ids; do + case "$intg" in + discord) + print_header "Discord Setup" + run_discord_shared + run_discord_agent "$agent_id" "$agent_name" + ;; + github) + print_header "GitHub Setup" + run_github + ;; + claude) + print_header "Claude Setup" + run_claude + ;; + gws) + print_header "Google Workspace Setup" + run_gws + ;; + xurl) + print_header "X/Twitter Setup" + run_xurl + ;; + esac + done + } + + # --- Reconfigure: summary --- + run_reconfigure_summary() { + # Create runtime directories + mkdir -p \ + "$SCRIPT_DIR/home/.openclaw/workspace" \ + "$SCRIPT_DIR/home/.claude" \ + "$SCRIPT_DIR/home/.config/gh" \ + "$SCRIPT_DIR/home/.config/gws" + [ -f "$SCRIPT_DIR/home/.xurl" ] || touch "$SCRIPT_DIR/home/.xurl" + + echo "" + print_header "Configuration Updated" + echo "" + + gum style --foreground 212 "Configured agents:" + local all_agent_ids + all_agent_ids=$(jq -r '.agents[].id' "$MANIFEST") + for aid in $all_agent_ids; do + local aid_upper token aname + aid_upper=$(echo "$aid" | tr '[:lower:]' '[:upper:]') + token=$(env_get "DISCORD_${aid_upper}_TOKEN") + if [ -n "$token" ]; then + aname=$(jq -r --arg id "$aid" '.agents[] | select(.id == $id) | .name' "$MANIFEST") + echo " ✓ $aname" + fi + done + echo "" + + gum style --foreground 212 "Integrations:" + local gh_token claude_token x_token + gh_token=$(env_get "GH_TOKEN") + [ -n "$gh_token" ] && echo " ✓ GitHub (configured)" || true + claude_token=$(env_get "CLAUDE_CODE_OAUTH_TOKEN") + [ -n "$claude_token" ] && echo " ✓ Claude Code (configured)" || true + [ -f "$SCRIPT_DIR/home/.config/gws/credentials.json" ] && echo " ✓ Google Workspace (configured)" || true + x_token=$(env_get "X_BEARER_TOKEN") + [ -n "$x_token" ] && echo " ✓ X/Twitter (configured)" || true + echo "" + + if gum_confirm "Restart OpenClaw now? (make restart)"; then + print_info "Running make restart..." + make -C "$SCRIPT_DIR" restart + else + print_info "Run 'make restart' to apply changes." + fi + } + + run_reconfigure_menu + run_reconfigure_summary + exit 0 fi # --- Screen 2: Agent Selection --- @@ -140,26 +411,7 @@ agents_for_integration() { # --- Screen 3: Git Identity (only for Forge) --- case " $SELECTED_AGENT_IDS " in *" forge "*) - print_header "Git Identity" - print_info "Used for commits made by Forge." - echo "" - - EXISTING_NAME=$(env_get "GIT_AUTHOR_NAME") - EXISTING_EMAIL=$(env_get "GIT_AUTHOR_EMAIL") - - GIT_NAME=$(gum_input "Full name" "${EXISTING_NAME:-Your Name}") - if [ -z "$GIT_NAME" ] && [ -n "$EXISTING_NAME" ]; then - GIT_NAME="$EXISTING_NAME" - fi - - GIT_EMAIL=$(gum_input "Email address" "${EXISTING_EMAIL:-you@example.com}") - if [ -z "$GIT_EMAIL" ] && [ -n "$EXISTING_EMAIL" ]; then - GIT_EMAIL="$EXISTING_EMAIL" - fi - - env_set "GIT_AUTHOR_NAME" "$GIT_NAME" - env_set "GIT_AUTHOR_EMAIL" "$GIT_EMAIL" - echo "" + _run_git_identity ;; esac diff --git a/tests/reconfigure.bats b/tests/reconfigure.bats new file mode 100644 index 0000000..e2a913a --- /dev/null +++ b/tests/reconfigure.bats @@ -0,0 +1,181 @@ +#!/usr/bin/env bats +# Tests for _integration_status helper in install.sh + +load test_helper + +setup() { + setup_tmpdir + export SCRIPT_DIR="$TEST_TMPDIR" + # Stub gum so lib.sh can be sourced without it installed + gum() { :; } + export -f gum + source "$PROJECT_ROOT/installer/lib.sh" + + # Define _integration_status as extracted from install.sh + _integration_status() { + local agent_id="$1" + local intg="$2" + local agent_upper + agent_upper=$(echo "$agent_id" | tr '[:lower:]' '[:upper:]') + case "$intg" in + discord) + local token + token=$(env_get "DISCORD_${agent_upper}_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + github) + local token + token=$(env_get "GH_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + claude) + local token + token=$(env_get "CLAUDE_CODE_OAUTH_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + gws) + [ -f "$SCRIPT_DIR/home/.config/gws/credentials.json" ] && echo "configured" || echo "not configured" + ;; + xurl) + local token + token=$(env_get "X_BEARER_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + *) + echo "not configured" + ;; + esac + } + export -f _integration_status +} + +teardown() { + teardown_tmpdir +} + +# --------------------------------------------------------------------------- +# discord +# --------------------------------------------------------------------------- + +@test "_integration_status: discord configured when DISCORD_FORGE_TOKEN set" { + echo "DISCORD_FORGE_TOKEN=some-token" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "discord" + [ "$output" = "configured" ] +} + +@test "_integration_status: discord not configured when DISCORD_FORGE_TOKEN empty" { + echo "DISCORD_FORGE_TOKEN=" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "discord" + [ "$output" = "not configured" ] +} + +@test "_integration_status: discord not configured when DISCORD_FORGE_TOKEN absent" { + run _integration_status "forge" "discord" + [ "$output" = "not configured" ] +} + +@test "_integration_status: discord uses agent id uppercased (scouter)" { + echo "DISCORD_SCOUTER_TOKEN=abc123" > "$TEST_TMPDIR/.env" + run _integration_status "scouter" "discord" + [ "$output" = "configured" ] +} + +# --------------------------------------------------------------------------- +# github +# --------------------------------------------------------------------------- + +@test "_integration_status: github configured when GH_TOKEN set" { + echo "GH_TOKEN=ghp_abc123" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "github" + [ "$output" = "configured" ] +} + +@test "_integration_status: github not configured when GH_TOKEN empty" { + echo "GH_TOKEN=" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "github" + [ "$output" = "not configured" ] +} + +@test "_integration_status: github not configured when GH_TOKEN absent" { + run _integration_status "forge" "github" + [ "$output" = "not configured" ] +} + +# --------------------------------------------------------------------------- +# claude +# --------------------------------------------------------------------------- + +@test "_integration_status: claude configured when CLAUDE_CODE_OAUTH_TOKEN set" { + echo "CLAUDE_CODE_OAUTH_TOKEN=oauth-token-xyz" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "claude" + [ "$output" = "configured" ] +} + +@test "_integration_status: claude not configured when CLAUDE_CODE_OAUTH_TOKEN empty" { + echo "CLAUDE_CODE_OAUTH_TOKEN=" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "claude" + [ "$output" = "not configured" ] +} + +@test "_integration_status: claude not configured when CLAUDE_CODE_OAUTH_TOKEN absent" { + run _integration_status "forge" "claude" + [ "$output" = "not configured" ] +} + +# --------------------------------------------------------------------------- +# gws +# --------------------------------------------------------------------------- + +@test "_integration_status: gws configured when credentials.json exists" { + mkdir -p "$TEST_TMPDIR/home/.config/gws" + touch "$TEST_TMPDIR/home/.config/gws/credentials.json" + run _integration_status "forge" "gws" + [ "$output" = "configured" ] +} + +@test "_integration_status: gws not configured when credentials.json absent" { + run _integration_status "forge" "gws" + [ "$output" = "not configured" ] +} + +@test "_integration_status: gws not configured when config dir exists but file missing" { + mkdir -p "$TEST_TMPDIR/home/.config/gws" + run _integration_status "forge" "gws" + [ "$output" = "not configured" ] +} + +# --------------------------------------------------------------------------- +# xurl +# --------------------------------------------------------------------------- + +@test "_integration_status: xurl configured when X_BEARER_TOKEN set" { + echo "X_BEARER_TOKEN=bearer-xyz" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "xurl" + [ "$output" = "configured" ] +} + +@test "_integration_status: xurl not configured when X_BEARER_TOKEN empty" { + echo "X_BEARER_TOKEN=" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "xurl" + [ "$output" = "not configured" ] +} + +@test "_integration_status: xurl not configured when X_BEARER_TOKEN absent" { + run _integration_status "forge" "xurl" + [ "$output" = "not configured" ] +} + +# --------------------------------------------------------------------------- +# unknown integration +# --------------------------------------------------------------------------- + +@test "_integration_status: unknown integration returns not configured" { + run _integration_status "forge" "nonexistent" + [ "$output" = "not configured" ] +} + +@test "_integration_status: unknown integration with token in env still returns not configured" { + echo "SOME_TOKEN=value" > "$TEST_TMPDIR/.env" + run _integration_status "forge" "unknown_intg" + [ "$output" = "not configured" ] +} From 7b7a215eed117c04dcd728df9ae3822c688ee49e Mon Sep 17 00:00:00 2001 From: Daniel Sierra Date: Wed, 25 Mar 2026 16:47:24 +0000 Subject: [PATCH 2/6] fix: address code review findings --- install.sh | 35 ++++++++++++++++++++--------------- tests/reconfigure.bats | 4 +++- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/install.sh b/install.sh index 450cc18..22e0c30 100755 --- a/install.sh +++ b/install.sh @@ -174,7 +174,9 @@ if [ "$RECONFIG" -eq 1 ]; then _run_git_identity ;; "Discord") - run_discord_shared + if [ -z "$(env_get "DISCORD_GUILD")" ]; then + run_discord_shared + fi run_discord_agent "$agent_id" "$agent_name" ;; "GitHub") @@ -195,21 +197,24 @@ if [ "$RECONFIG" -eq 1 ]; then # --- Reconfigure: top-level menu --- run_reconfigure_menu() { + # Build id|name pairs once; reused for menu rendering and choice resolution + local agent_map + agent_map=$(jq -r '.agents[] | .id + "|" + .name' "$MANIFEST") + while true; do local menu_items="" - local all_agent_ids - all_agent_ids=$(jq -r '.agents[].id' "$MANIFEST") - for aid in $all_agent_ids; do - local aid_upper aname token_val + local aid aname aid_upper token_val + while IFS='|' read -r aid aname; do aid_upper=$(echo "$aid" | tr '[:lower:]' '[:upper:]') - aname=$(jq -r --arg id "$aid" '.agents[] | select(.id == $id) | .name' "$MANIFEST") token_val=$(env_get "DISCORD_${aid_upper}_TOKEN") if [ -n "$token_val" ]; then menu_items="${menu_items}${aname}\n" else menu_items="${menu_items}${aname} (not installed)\n" fi - done + done < Date: Wed, 25 Mar 2026 22:13:43 +0100 Subject: [PATCH 3/6] fix: move _integration_status to lib.sh and fix sed regex Extract _integration_status into installer/lib.sh so tests source the real function instead of a duplicated copy. Switch sed to -E (ERE) for correct parentheses matching in the reconfigure submenu. Co-Authored-By: Claude Opus 4.6 (1M context) --- install.sh | 39 +-------------------------------------- installer/lib.sh | 40 ++++++++++++++++++++++++++++++++++++++++ tests/reconfigure.bats | 38 -------------------------------------- 3 files changed, 41 insertions(+), 76 deletions(-) diff --git a/install.sh b/install.sh index 22e0c30..4441fa8 100755 --- a/install.sh +++ b/install.sh @@ -26,43 +26,6 @@ if [ ! -f "$MANIFEST" ]; then exit 1 fi -# --- Helper: integration status --- -# _integration_status AGENT_ID INTEGRATION_KEY — returns "configured" or "not configured" -_integration_status() { - local agent_id="$1" - local intg="$2" - local agent_upper - agent_upper=$(echo "$agent_id" | tr '[:lower:]' '[:upper:]') - case "$intg" in - discord) - local token - token=$(env_get "DISCORD_${agent_upper}_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - github) - local token - token=$(env_get "GH_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - claude) - local token - token=$(env_get "CLAUDE_CODE_OAUTH_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - gws) - [ -f "$SCRIPT_DIR/home/.config/gws/credentials.json" ] && echo "configured" || echo "not configured" - ;; - xurl) - local token - token=$(env_get "X_BEARER_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - *) - echo "not configured" - ;; - esac -} - # --- Helper: git identity prompts --- _run_git_identity() { print_header "Git Identity" @@ -168,7 +131,7 @@ if [ "$RECONFIG" -eq 1 ]; then # Strip " (status)" suffix to get the label local label - label=$(echo "$choice" | sed 's/ ([^)]*)$//') + label=$(echo "$choice" | sed -E 's/ \([^)]*\)$//') case "$label" in "Git Identity") _run_git_identity diff --git a/installer/lib.sh b/installer/lib.sh index 5575630..f170d92 100644 --- a/installer/lib.sh +++ b/installer/lib.sh @@ -189,3 +189,43 @@ env_blank() { local key="$1" env_set "$key" "" } + +# --------------------------------------------------------------------------- +# Integration status +# --------------------------------------------------------------------------- + +# _integration_status AGENT_ID INTEGRATION_KEY — returns "configured" or "not configured" +_integration_status() { + local agent_id="$1" + local intg="$2" + local agent_upper + agent_upper=$(echo "$agent_id" | tr '[:lower:]' '[:upper:]') + case "$intg" in + discord) + local token + token=$(env_get "DISCORD_${agent_upper}_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + github) + local token + token=$(env_get "GH_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + claude) + local token + token=$(env_get "CLAUDE_CODE_OAUTH_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + gws) + [ -f "$SCRIPT_DIR/home/.config/gws/credentials.json" ] && echo "configured" || echo "not configured" + ;; + xurl) + local token + token=$(env_get "X_BEARER_TOKEN") + [ -n "$token" ] && echo "configured" || echo "not configured" + ;; + *) + echo "not configured" + ;; + esac +} diff --git a/tests/reconfigure.bats b/tests/reconfigure.bats index 99af7a5..5777ae4 100644 --- a/tests/reconfigure.bats +++ b/tests/reconfigure.bats @@ -10,44 +10,6 @@ setup() { gum() { :; } export -f gum source "$PROJECT_ROOT/installer/lib.sh" - - # Define _integration_status extracted from install.sh (keep in sync with - # the definition in install.sh — search for "_integration_status()" there). - # install.sh cannot be sourced wholesale due to top-level side effects. - _integration_status() { - local agent_id="$1" - local intg="$2" - local agent_upper - agent_upper=$(echo "$agent_id" | tr '[:lower:]' '[:upper:]') - case "$intg" in - discord) - local token - token=$(env_get "DISCORD_${agent_upper}_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - github) - local token - token=$(env_get "GH_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - claude) - local token - token=$(env_get "CLAUDE_CODE_OAUTH_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - gws) - [ -f "$SCRIPT_DIR/home/.config/gws/credentials.json" ] && echo "configured" || echo "not configured" - ;; - xurl) - local token - token=$(env_get "X_BEARER_TOKEN") - [ -n "$token" ] && echo "configured" || echo "not configured" - ;; - *) - echo "not configured" - ;; - esac - } export -f _integration_status } From 1f26e1542f3647cdeb42b09394523d33578cea1e Mon Sep 17 00:00:00 2001 From: Daniel Sierra Ramos Date: Wed, 25 Mar 2026 23:18:20 +0100 Subject: [PATCH 4/6] docs: initialize project --- .planning/PROJECT.md | 77 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .planning/PROJECT.md diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md new file mode 100644 index 0000000..16372e5 --- /dev/null +++ b/.planning/PROJECT.md @@ -0,0 +1,77 @@ +# CrewDock: Obsidian Vault Sync + +## What This Is + +A Syncthing sidecar container for CrewDock that provides bidirectional sync with the user's Obsidian vault. All agents (Alfred, Forge, Scouter, Overlord) gain read/write access to the vault as a shared knowledge base, mounted at `/home/node/vault`. + +## Core Value + +Every agent in CrewDock can read and write to the user's Obsidian vault in real time, keeping the knowledge base synchronized bidirectionally with the user's local Obsidian instance. + +## Requirements + +### Validated + +(None yet — ship to validate) + +### Active + +- [ ] Syncthing runs as a Docker sidecar container alongside existing CrewDock services +- [ ] Vault mounts at `/home/node/vault` inside the main container, accessible by all agents +- [ ] Bidirectional sync: agents can read and write notes that sync back to local Obsidian +- [ ] install.sh wizard includes an optional step to enable vault sync +- [ ] Minimal config: installer only asks for local vault path on the host +- [ ] Syncthing peers with the user's existing Syncthing instance +- [ ] Sync works with the full vault (no folder filtering) + +### Out of Scope + +- Per-agent vault access control — all agents share the same mount +- Vault folder filtering or partial sync — full vault only for v1 +- Conflict resolution UI — rely on Syncthing's built-in conflict handling +- Obsidian plugin integration — this is file-level sync, not plugin-level + +## Context + +- CrewDock runs 4 agents: Alfred (GWS assistant), Forge (dev orchestrator), Scouter (content radar), Overlord (sysadmin) +- User already runs Syncthing to sync the Obsidian vault across devices +- The vault lives at `/Users/dasirra/Vault` locally (the "Second Brain") +- Docker setup uses host networking, single persistent volume at `./home` -> `/home/node` +- install.sh is a TUI wizard with modular setup scripts under `installer/` +- The existing install flow supports selective reconfiguration of integrations + +## Constraints + +- **Docker**: Syncthing must run as a separate container (sidecar), not inside the main OpenClaw container +- **Networking**: CrewDock uses host networking; Syncthing needs ports 8384 (web UI, optional) and 22000 (sync protocol) +- **Permissions**: Container user is `node`; vault files must be readable/writable by this user +- **Existing Syncthing**: Must peer with an already-running Syncthing instance, not replace it + +## Key Decisions + +| Decision | Rationale | Outcome | +|----------|-----------|---------| +| Sidecar container vs in-container install | Separation of concerns, independent updates, cleaner Docker architecture | — Pending | +| Mount at /home/node/vault | Consistent with existing home volume pattern, accessible by all agents | — Pending | +| Optional install step | Not all users have Obsidian; keep core CrewDock lightweight | — Pending | +| Minimal config (vault path only) | Reduce friction; Syncthing device pairing can happen via web UI post-install | — Pending | + +## Evolution + +This document evolves at phase transitions and milestone boundaries. + +**After each phase transition** (via `/gsd:transition`): +1. Requirements invalidated? -> Move to Out of Scope with reason +2. Requirements validated? -> Move to Validated with phase reference +3. New requirements emerged? -> Add to Active +4. Decisions to log? -> Add to Key Decisions +5. "What This Is" still accurate? -> Update if drifted + +**After each milestone** (via `/gsd:complete-milestone`): +1. Full review of all sections +2. Core Value check — still the right priority? +3. Audit Out of Scope — reasons still valid? +4. Update Context with current state + +--- +*Last updated: 2026-03-25 after initialization* From d9dade11751f0581a032427e253118424aa9942c Mon Sep 17 00:00:00 2001 From: Daniel Sierra Ramos Date: Wed, 25 Mar 2026 23:19:42 +0100 Subject: [PATCH 5/6] chore: add project config --- .planning/config.json | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .planning/config.json diff --git a/.planning/config.json b/.planning/config.json new file mode 100644 index 0000000..cd614aa --- /dev/null +++ b/.planning/config.json @@ -0,0 +1,35 @@ +{ + "model_profile": "balanced", + "commit_docs": true, + "parallelization": true, + "search_gitignored": false, + "brave_search": false, + "firecrawl": false, + "exa_search": false, + "git": { + "branching_strategy": "none", + "phase_branch_template": "gsd/phase-{phase}-{slug}", + "milestone_branch_template": "gsd/{milestone}-{slug}", + "quick_branch_template": null + }, + "workflow": { + "research": false, + "plan_check": true, + "verifier": true, + "nyquist_validation": true, + "auto_advance": false, + "node_repair": true, + "node_repair_budget": 2, + "ui_phase": true, + "ui_safety_gate": true, + "text_mode": false, + "research_before_questions": false, + "discuss_mode": "discuss", + "skip_discuss": false + }, + "hooks": { + "context_warnings": true + }, + "mode": "yolo", + "granularity": "standard" +} \ No newline at end of file From a9d2816dc4f98bc1c58ff9be41f9b8d20314bd63 Mon Sep 17 00:00:00 2001 From: Daniel Sierra Ramos Date: Thu, 26 Mar 2026 09:40:05 +0100 Subject: [PATCH 6/6] fix: harden reconfigure menu dispatch, validation, and summary - Resolve submenu labels via manifest lookup instead of hardcoded strings - Validate non-empty git name/email before saving in _run_git_identity - Add Discord status to reconfigure summary - Ensure OPENCLAW_GATEWAY_TOKEN exists in .env during reconfigure Co-Authored-By: Claude Opus 4.6 (1M context) --- install.sh | 65 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/install.sh b/install.sh index 4441fa8..fa34273 100755 --- a/install.sh +++ b/install.sh @@ -42,6 +42,11 @@ _run_git_identity() { if [ -z "$git_email" ] && [ -n "$existing_email" ]; then git_email="$existing_email" fi + if [ -z "$git_name" ] || [ -z "$git_email" ]; then + print_warn "Git name and email are required. Skipping." + echo "" + return + fi env_set "GIT_AUTHOR_NAME" "$git_name" env_set "GIT_AUTHOR_EMAIL" "$git_email" echo "" @@ -132,29 +137,34 @@ if [ "$RECONFIG" -eq 1 ]; then # Strip " (status)" suffix to get the label local label label=$(echo "$choice" | sed -E 's/ \([^)]*\)$//') - case "$label" in - "Git Identity") - _run_git_identity - ;; - "Discord") - if [ -z "$(env_get "DISCORD_GUILD")" ]; then - run_discord_shared - fi - run_discord_agent "$agent_id" "$agent_name" - ;; - "GitHub") - run_github - ;; - "Claude Code") - run_claude - ;; - "Google Workspace") - run_gws - ;; - "X/Twitter") - run_xurl - ;; - esac + if [ "$label" = "Git Identity" ]; then + _run_git_identity + else + # Resolve label back to integration key via manifest + local intg_key + intg_key=$(jq -r --arg label "$label" \ + '.integrations | to_entries[] | select(.value.label == $label) | .key' "$MANIFEST") + case "$intg_key" in + discord) + if [ -z "$(env_get "DISCORD_GUILD")" ]; then + run_discord_shared + fi + run_discord_agent "$agent_id" "$agent_name" + ;; + github) + run_github + ;; + claude) + run_claude + ;; + gws) + run_gws + ;; + xurl) + run_xurl + ;; + esac + fi done } @@ -256,6 +266,11 @@ EOF # --- Reconfigure: summary --- run_reconfigure_summary() { + # Ensure gateway token key exists (auto-generated on boot) + if ! grep -qE "^OPENCLAW_GATEWAY_TOKEN=" "$SCRIPT_DIR/.env" 2>/dev/null; then + env_set "OPENCLAW_GATEWAY_TOKEN" "" + fi + # Create runtime directories mkdir -p \ "$SCRIPT_DIR/home/.openclaw/workspace" \ @@ -283,7 +298,9 @@ EOF echo "" gum style --foreground 212 "Integrations:" - local gh_token claude_token x_token + local discord_guild gh_token claude_token x_token + discord_guild=$(env_get "DISCORD_GUILD") + [ -n "$discord_guild" ] && echo " ✓ Discord (configured)" || true gh_token=$(env_get "GH_TOKEN") [ -n "$gh_token" ] && echo " ✓ GitHub (configured)" || true claude_token=$(env_get "CLAUDE_CODE_OAUTH_TOKEN")