#!/usr/bin/env bash # Dialog Machine — Claude Code Installer # Handles auth, installs hooks, registers agent. Idempotent. # # Usage: curl -fsSL https://dialogmachine.com/install/claude-code.sh | bash set -euo pipefail export DIALOGMACHINE_INSTALL_BASE="${DIALOGMACHINE_INSTALL_BASE:-https://www.dialogmachine.com/install}" # --- _common.sh (inlined) --- #!/usr/bin/env bash # Dialog Machine — shared installer functions # Sourced by per-agent install scripts. set -euo pipefail # ---- Colors ---- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' # ---- Config ---- DM_HOME="$HOME/.dialogmachine" DM_CONFIG="$DM_HOME/config.json" DM_HOOKS_DIR="$DM_HOME/hooks" API_BASE="${DIALOGMACHINE_API_BASE:-https://api.dialogmachine.com}" WITHMD_API="https://with.md/api/public/share" # ---- Banner ---- dm_banner() { echo "" echo -e "${BOLD}${CYAN} ┌──────────────────────────────────┐${RESET}" echo -e "${BOLD}${CYAN} │ Dialog Machine v0.2 │${RESET}" echo -e "${BOLD}${CYAN} │ Per-Agent Installer │${RESET}" echo -e "${BOLD}${CYAN} └──────────────────────────────────┘${RESET}" echo "" } # ---- Prerequisites ---- dm_check_prereqs() { local MISSING=() for cmd in jq curl; do if ! command -v "$cmd" &>/dev/null; then MISSING+=("$cmd") fi done if [ ${#MISSING[@]} -gt 0 ]; then echo -e "${RED}Error: missing required tools: ${MISSING[*]}${RESET}" >&2 echo "Install them and retry." >&2 exit 1 fi } # ---- Directories ---- dm_ensure_dirs() { mkdir -p "$DM_HOME" "$DM_HOOKS_DIR" "$DM_HOME/agents" } # ---- Config helpers ---- dm_read_config() { local key="$1" if [ -f "$DM_CONFIG" ]; then jq -r "$key // empty" "$DM_CONFIG" 2>/dev/null || true fi } dm_ensure_config() { # Create config if it doesn't exist if [ ! -f "$DM_CONFIG" ]; then jq -n \ --arg hostname "$(hostname -s 2>/dev/null || echo unknown)" \ --arg api_base "$API_BASE" '{ version: 2, api_base: $api_base, hostname: $hostname, installation_id: null, machine_token: null, agents: {} }' > "$DM_CONFIG" fi } dm_save_config() { # Merge key-value pairs into config. Usage: dm_save_config '.key = "value"' # IMPORTANT: For server-returned values, use dm_save_config_args instead. local expr="$1" dm_ensure_config local tmp="${DM_CONFIG}.tmp.$" if jq "$expr" "$DM_CONFIG" > "$tmp" 2>/dev/null; then mv "$tmp" "$DM_CONFIG" else rm -f "$tmp" echo -e "${RED}Failed to update config${RESET}" >&2 fi } dm_save_auth() { # Safely save auth credentials using --arg to prevent injection local inst_id="$1" token="$2" base="$3" dm_ensure_config local tmp="${DM_CONFIG}.tmp.$" if jq \ --arg id "$inst_id" \ --arg tok "$token" \ --arg base "$base" ' .installation_id = $id | .machine_token = $tok | .api_base = $base ' "$DM_CONFIG" > "$tmp" 2>/dev/null; then mv "$tmp" "$DM_CONFIG" else rm -f "$tmp" echo -e "${RED}Failed to save auth credentials${RESET}" >&2 fi } dm_save_agent_config() { # Safely save agent config using --arg local agent="$1" config_dir="$2" dm_ensure_config local tmp="${DM_CONFIG}.tmp.$" if jq \ --arg agent "$agent" \ --arg dir "$config_dir" ' .agents[$agent].config_dir = $dir | .agents[$agent].hook_installed = (.agents[$agent].hook_installed // false) | .agents[$agent].withmd = (.agents[$agent].withmd // {share_id: null, edit_secret: null, view_url: null}) ' "$DM_CONFIG" > "$tmp" 2>/dev/null; then mv "$tmp" "$DM_CONFIG" else rm -f "$tmp" fi } # ---- Device Code Auth ---- dm_auth() { # Check for existing credentials local installation_id machine_token installation_id=$(dm_read_config '.installation_id') machine_token=$(dm_read_config '.machine_token') if [ -n "$installation_id" ] && [ -n "$machine_token" ]; then API_BASE=$(dm_read_config '.api_base') [ -z "$API_BASE" ] && API_BASE="${DIALOGMACHINE_API_BASE:-https://api.dialogmachine.com}" # Verify credentials are still valid local verify_code verify_code=$(curl -s -o /dev/null -w "%{http_code}" \ "$API_BASE/v1/installations/$installation_id/heartbeat" \ -X POST \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ -d '{"agent":"_ping","event":"verify"}' \ --connect-timeout 3 --max-time 5 2>/dev/null) || verify_code="000" if [ "$verify_code" = "200" ] || [ "$verify_code" = "201" ]; then echo -e " ${GREEN}✓${RESET} Authenticated ${DIM}(reusing existing credentials)${RESET}" return 0 fi echo -e " ${YELLOW}Stale credentials detected — re-authenticating...${RESET}" # Clear stale credentials so we go through the device code flow dm_save_auth "" "" "$API_BASE" installation_id="" machine_token="" fi echo -e "${BOLD}Authenticating with Dialog Machine...${RESET}" # Request device code local response http_code body response=$(curl -s -w "\n%{http_code}" -X POST "$API_BASE/v1/device/code" \ -H "Content-Type: application/json" \ --connect-timeout 5 --max-time 10 \ -d "$(jq -n --arg hostname "$(hostname -s 2>/dev/null || echo unknown)" \ '{hostname: $hostname}')" \ 2>/dev/null) || { echo -e " ${YELLOW}Warning:${RESET} backend unreachable at ${DIM}$API_BASE${RESET}. Auth skipped." return 1 } http_code=$(echo "$response" | tail -1) body=$(printf '%s' "$response" | sed '$d') if [ "$http_code" != "200" ]; then echo -e " ${RED}Error:${RESET} failed to get device code (HTTP $http_code)" return 1 fi local device_code user_code verification_uri_complete expires_in interval device_code=$(echo "$body" | jq -r '.device_code') user_code=$(echo "$body" | jq -r '.user_code') verification_uri_complete=$(echo "$body" | jq -r '.verification_uri_complete') expires_in=$(echo "$body" | jq -r '.expires_in') interval=$(echo "$body" | jq -r '.interval') echo "" echo -e " ${BOLD}Open this URL to authenticate:${RESET}" echo "" echo -e " ${CYAN}${verification_uri_complete}${RESET}" echo "" # Try to open browser if command -v xdg-open &>/dev/null; then xdg-open "$verification_uri_complete" 2>/dev/null & elif command -v open &>/dev/null; then open "$verification_uri_complete" 2>/dev/null & fi # Poll for approval echo -e " ${DIM}Waiting for browser confirmation...${RESET}" local elapsed=0 while [ "$elapsed" -lt "$expires_in" ]; do sleep "$interval" elapsed=$((elapsed + interval)) response=$(curl -s -w "\n%{http_code}" -X POST "$API_BASE/v1/device/token" \ -H "Content-Type: application/json" \ --connect-timeout 5 --max-time 10 \ -d "$(jq -n --arg dc "$device_code" '{device_code: $dc}')" \ 2>/dev/null) || continue http_code=$(echo "$response" | tail -1) body=$(echo "$response" | sed '$d') if [ "$http_code" != "200" ]; then continue fi local error_field error_field=$(echo "$body" | jq -r '.error // empty') if [ "$error_field" = "authorization_pending" ]; then continue fi if [ "$error_field" = "expired_token" ]; then echo -e " ${RED}Error:${RESET} device code expired. Please try again." return 1 fi if [ "$error_field" = "already_issued" ]; then echo -e " ${YELLOW}Warning:${RESET} credentials were already issued but not saved locally. Please re-run." return 1 fi if [ -z "$error_field" ]; then # Success! installation_id=$(echo "$body" | jq -r '.installation_id') machine_token=$(echo "$body" | jq -r '.machine_token') local api_base_url api_base_url=$(echo "$body" | jq -r '.api_base_url // empty') if [ -n "$installation_id" ] && [ -n "$machine_token" ]; then dm_save_auth "$installation_id" "$machine_token" "${api_base_url:-$API_BASE}" API_BASE="${api_base_url:-$API_BASE}" echo -e " ${GREEN}✓${RESET} Authenticated! Installation: ${DIM}$installation_id${RESET}" return 0 fi fi done echo -e " ${RED}Error:${RESET} timed out waiting for approval." return 1 } # ---- Agent Registration ---- dm_register_agent() { local name="$1" local config_dir="${2:-}" local hook_installed="${3:-false}" local withmd_view_url="${4:-}" local installation_id machine_token installation_id=$(dm_read_config '.installation_id') machine_token=$(dm_read_config '.machine_token') if [ -z "$installation_id" ] || [ -z "$machine_token" ]; then echo -e " ${DIM}Skipping agent registration (not authenticated)${RESET}" return 0 fi local payload payload=$(jq -n \ --arg name "$name" \ --arg config_dir "$config_dir" \ --argjson hook_installed "$hook_installed" \ --arg withmd "$withmd_view_url" '{ name: $name, config_dir: (if $config_dir != "" then $config_dir else null end), hook_installed: $hook_installed, withmd_view_url: (if $withmd != "" then $withmd else null end) }') local response http_code response=$(curl -s -w "\n%{http_code}" -X POST \ "$API_BASE/v1/installations/$installation_id/agents" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 5 --max-time 10 \ -d "$payload" \ 2>/dev/null) || { echo -e " ${YELLOW}Warning:${RESET} agent registration failed (network error)" return 0 } http_code=$(echo "$response" | tail -1) if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then echo -e " ${GREEN}✓${RESET} Registered agent: ${BOLD}$name${RESET}" else echo -e " ${YELLOW}Warning:${RESET} agent registration returned HTTP $http_code" fi } # ---- Bootstrap helpers ---- dm_file_mtime_unix() { local file_path="$1" if [ ! -f "$file_path" ]; then echo "0" return 0 fi if stat -f "%m" "$file_path" >/dev/null 2>&1; then stat -f "%m" "$file_path" 2>/dev/null || echo "0" return 0 fi stat -c "%Y" "$file_path" 2>/dev/null || echo "0" } dm_bootstrap_summarize_history() { local installation_id="$1" local machine_token="$2" local api_base="$3" local agent_name="$4" local history_items_json="${5:-[]}" local history_meta_json="${6-}" [ -z "$history_meta_json" ] && history_meta_json='{}' local tmp_items tmp_meta tmp_items=$(mktemp) tmp_meta=$(mktemp) printf '%s' "$history_items_json" > "$tmp_items" printf '%s' "$history_meta_json" > "$tmp_meta" local payload payload=$(jq -n \ --slurpfile history_items "$tmp_items" \ --slurpfile history_meta "$tmp_meta" '{ history_items: ($history_items[0] // []), history_meta: ($history_meta[0] // {}) }') rm -f "$tmp_items" "$tmp_meta" local response http_code body response=$(curl -s -w "\n%{http_code}" -X POST \ "$api_base/v1/installations/$installation_id/agents/$agent_name/bootstrap/summarize" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 60 \ -d "$payload" \ 2>/dev/null) || return 1 http_code=$(printf '%s' "$response" | tail -1) body=$(printf '%s' "$response" | sed '$d') if [ "$http_code" != "200" ]; then return 1 fi echo "$body" } dm_bootstrap_enqueue() { local installation_id="$1" local machine_token="$2" local api_base="$3" local agent_name="$4" local payload_json="$5" local response http_code body response=$(curl -s -w "\n%{http_code}" -X POST \ "$api_base/v1/installations/$installation_id/agents/$agent_name/bootstrap" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$payload_json" \ 2>/dev/null) || return 1 http_code=$(printf '%s' "$response" | tail -1) body=$(printf '%s' "$response" | sed '$d') if [ "$http_code" != "200" ] && [ "$http_code" != "201" ]; then return 1 fi echo "$body" } dm_bootstrap_status() { local installation_id="$1" local machine_token="$2" local api_base="$3" local agent_name="$4" local job_id="$5" local response http_code body response=$(curl -s -w "\n%{http_code}" -X GET \ "$api_base/v1/installations/$installation_id/agents/$agent_name/bootstrap/status?job_id=$job_id" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 20 \ 2>/dev/null) || return 1 http_code=$(echo "$response" | tail -1) body=$(echo "$response" | sed '$d') if [ "$http_code" != "200" ]; then return 1 fi echo "$body" } dm_bootstrap_wait_completion() { local installation_id="$1" local machine_token="$2" local api_base="$3" local agent_name="$4" local job_id="$5" local attempt=0 local max_attempts=60 local status_response="" local status_value="" while [ "$attempt" -lt "$max_attempts" ]; do attempt=$((attempt + 1)) status_response=$(dm_bootstrap_status "$installation_id" "$machine_token" "$api_base" "$agent_name" "$job_id") || { sleep 2 continue } status_value=$(echo "$status_response" | jq -r '.status // empty' 2>/dev/null) if [ "$status_value" = "completed" ] || [ "$status_value" = "failed" ]; then echo "$status_response" return 0 fi sleep 2 done [ -n "$status_response" ] && echo "$status_response" return 1 } dm_save_bootstrap_state() { local agent_name="$1" local job_id="${2:-}" local status_value="${3:-}" local provider="${4:-}" dm_ensure_config local tmp="${DM_CONFIG}.tmp.$" if jq \ --arg agent "$agent_name" \ --arg job "$job_id" \ --arg status "$status_value" \ --arg provider "$provider" ' .agents[$agent].bootstrap = { last_job_id: (if $job != "" then $job else null end), last_status: (if $status != "" then $status else null end), summary_provider: (if $provider != "" then $provider else null end) } ' "$DM_CONFIG" > "$tmp" 2>/dev/null; then mv "$tmp" "$DM_CONFIG" else rm -f "$tmp" fi } dm_run_agent_bootstrap_flow() { local agent_name="$1" local history_items_json="${2:-[]}" local history_meta_json="${3-}" [ -z "$history_meta_json" ] && history_meta_json='{}' local soul_markdown="${4:-}" local identity_markdown="${5:-}" local user_markdown="${6:-}" if ! printf '%s' "$history_items_json" | jq -e 'type == "array"' >/dev/null 2>&1; then history_items_json="[]" fi if ! printf '%s' "$history_meta_json" | jq -e 'type == "object"' >/dev/null 2>&1; then history_meta_json="{}" fi local installation_id machine_token api_base installation_id=$(dm_read_config '.installation_id') machine_token=$(dm_read_config '.machine_token') api_base=$(dm_read_config '.api_base') [ -z "$api_base" ] && api_base="${API_BASE}" if [ -z "$installation_id" ] || [ -z "$machine_token" ]; then echo -e " ${DIM}Skipping bootstrap ingestion for ${agent_name} (not authenticated)${RESET}" return 0 fi local item_count item_count=$(printf '%s' "$history_items_json" | jq -r 'if type == "array" then length else 0 end' 2>/dev/null || echo "0") local history_summary="" local summary_provider="none" local effective_meta_json="$history_meta_json" if [ "$item_count" -gt 0 ]; then local summarize_response summarize_response=$(dm_bootstrap_summarize_history \ "$installation_id" \ "$machine_token" \ "$api_base" \ "$agent_name" \ "$history_items_json" \ "$history_meta_json") || summarize_response="" if [ -n "$summarize_response" ]; then history_summary=$(printf '%s' "$summarize_response" | jq -r '.history_summary // ""' 2>/dev/null) summary_provider=$(printf '%s' "$summarize_response" | jq -r '.provider // "none"' 2>/dev/null) effective_meta_json=$(printf '%s' "$summarize_response" | jq -c '.history_meta // {}' 2>/dev/null || echo "$history_meta_json") if ! printf '%s' "$effective_meta_json" | jq -e 'type == "object"' >/dev/null 2>&1; then effective_meta_json="$history_meta_json" fi if [ -n "$history_summary" ]; then echo -e " ${GREEN}✓${RESET} Built bootstrap summary via ${summary_provider}" fi else echo -e " ${YELLOW}Warning:${RESET} bootstrap summarization failed for ${agent_name}; continuing without summary." fi fi local bootstrap_payload local tmp_effective_meta tmp_effective_meta=$(mktemp) printf '%s' "$effective_meta_json" > "$tmp_effective_meta" bootstrap_payload=$(jq -n \ --arg soul "$soul_markdown" \ --arg identity "$identity_markdown" \ --arg user "$user_markdown" \ --arg history_summary "$history_summary" \ --slurpfile history_meta "$tmp_effective_meta" '{ soul_markdown: $soul, identity_markdown: $identity, user_markdown: $user, history_summary: $history_summary, history_meta: ($history_meta[0] // {}) }') rm -f "$tmp_effective_meta" local enqueue_response enqueue_response=$(dm_bootstrap_enqueue \ "$installation_id" \ "$machine_token" \ "$api_base" \ "$agent_name" \ "$bootstrap_payload") || { echo -e " ${YELLOW}Warning:${RESET} bootstrap ingestion enqueue failed for ${agent_name}." dm_save_bootstrap_state "$agent_name" "" "enqueue_failed" "$summary_provider" return 0 } local job_id status_value job_id=$(printf '%s' "$enqueue_response" | jq -r '.ingestion_job_id // empty' 2>/dev/null) status_value=$(printf '%s' "$enqueue_response" | jq -r '.status // empty' 2>/dev/null) if [ -z "$job_id" ]; then echo -e " ${YELLOW}Warning:${RESET} bootstrap ingestion response missing job id for ${agent_name}." dm_save_bootstrap_state "$agent_name" "" "missing_job_id" "$summary_provider" return 0 fi if [ "$status_value" = "queued" ] || [ "$status_value" = "processing" ]; then local final_status_response final_status_response=$(dm_bootstrap_wait_completion \ "$installation_id" \ "$machine_token" \ "$api_base" \ "$agent_name" \ "$job_id") || final_status_response="" if [ -n "$final_status_response" ]; then status_value=$(printf '%s' "$final_status_response" | jq -r '.status // empty' 2>/dev/null) fi fi if [ "$status_value" = "completed" ]; then echo -e " ${GREEN}✓${RESET} Bootstrap ingestion completed for ${BOLD}${agent_name}${RESET}" elif [ "$status_value" = "failed" ]; then echo -e " ${YELLOW}Warning:${RESET} bootstrap ingestion failed for ${agent_name}." else echo -e " ${YELLOW}Warning:${RESET} bootstrap ingestion status unknown for ${agent_name}." fi dm_save_bootstrap_state "$agent_name" "$job_id" "$status_value" "$summary_provider" return 0 } # ---- with.md sync helpers ---- dm_withmd_create() { local agent="$1" local content="$2" local title="$3" local response http_code body response=$(curl -s -w "\n%{http_code}" -X POST "$WITHMD_API/create" \ -H "Content-Type: application/json" \ --connect-timeout 5 --max-time 10 \ -d "$(jq -n --arg c "$content" --arg t "$title" \ '{content: $c, title: $t, expiresInHours: 720}')" \ 2>/dev/null) || return 0 http_code=$(echo "$response" | tail -1) body=$(echo "$response" | sed '$d') if [ "$http_code" = "201" ]; then local sid es vu sid=$(echo "$body" | jq -r '.shareId') es=$(echo "$body" | jq -r '.editSecret') vu=$(echo "$body" | jq -r '.viewUrl') dm_ensure_config local cfg_tmp="${DM_CONFIG}.tmp.$" jq --arg agent "$agent" --arg sid "$sid" --arg es "$es" --arg vu "$vu" ' .agents[$agent].withmd = {share_id: $sid, edit_secret: $es, view_url: $vu} ' "$DM_CONFIG" > "$cfg_tmp" 2>/dev/null && mv "$cfg_tmp" "$DM_CONFIG" echo "$vu" fi } # Upload session files to S3 and trigger summarization. # $1: agent_name (e.g. "claude-code" or "codex") # $2: path to manifest file (one JSON object per line with session_id, sha256, size_bytes, file_path, metadata_json) dm_sync_sessions_to_s3() { local agent_name="$1" local manifest_file="$2" local installation_id machine_token api_base installation_id=$(dm_read_config '.installation_id') machine_token=$(dm_read_config '.machine_token') api_base=$(dm_read_config '.api_base') [ -z "$api_base" ] && api_base="${API_BASE}" if [ -z "$installation_id" ] || [ -z "$machine_token" ]; then echo -e " ${DIM}Skipping session sync (not authenticated)${RESET}" return 0 fi local session_count session_count=$(wc -l < "$manifest_file" 2>/dev/null | tr -d ' ') if [ "$session_count" -eq 0 ]; then echo -e " ${DIM}No sessions found${RESET}" return 0 fi local total_uploaded=0 local total_skipped=0 local batch_start=1 while [ "$batch_start" -le "$session_count" ]; do local batch_end=$((batch_start + 99)) # Build batch JSON (strip file_path from manifest before sending) local batch_json batch_json=$(sed -n "${batch_start},${batch_end}p" "$manifest_file" | jq -s '[.[] | {session_id, sha256, size_bytes, metadata_json}]') # Step 1: Get upload URLs local urls_payload urls_response urls_code urls_body urls_payload=$(jq -cn --argjson s "$batch_json" '{ sessions: $s }') urls_response=$(curl -s -w "\n%{http_code}" -X POST \ "$api_base/v1/installations/$installation_id/agents/$agent_name/sessions/upload-urls" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$urls_payload" 2>/dev/null) || { batch_start=$((batch_end + 1)); continue; } urls_code=$(printf '%s' "$urls_response" | tail -1) urls_body=$(printf '%s' "$urls_response" | sed '$d') if [ "$urls_code" != "200" ]; then batch_start=$((batch_end + 1)) continue fi local batch_skipped batch_skipped=$(printf '%s' "$urls_body" | jq '[.results[]? | select(.skipped == true)] | length' 2>/dev/null || echo 0) total_skipped=$((total_skipped + batch_skipped)) # Step 2: Upload each non-skipped session to S3 printf '%s' "$urls_body" | jq -c '.results[]? | select(.skipped == false)' 2>/dev/null | while IFS= read -r item; do local sid upload_url s3_key sid=$(printf '%s' "$item" | jq -r '.session_id') upload_url=$(printf '%s' "$item" | jq -r '.upload_url') s3_key=$(printf '%s' "$item" | jq -r '.s3_key') [ -z "$upload_url" ] || [ "$upload_url" = "null" ] && continue # Find file_path from manifest local manifest_line file_path sha fsize meta_json manifest_line=$(jq -c "select(.session_id == \"$sid\")" "$manifest_file" 2>/dev/null | head -1) [ -z "$manifest_line" ] && continue file_path=$(printf '%s' "$manifest_line" | jq -r '.file_path') sha=$(printf '%s' "$manifest_line" | jq -r '.sha256') fsize=$(printf '%s' "$manifest_line" | jq -r '.size_bytes') meta_json=$(printf '%s' "$manifest_line" | jq -c '.metadata_json') [ ! -f "$file_path" ] && continue # Upload to S3 curl -s -X PUT "$upload_url" \ -H "Content-Type: application/x-ndjson" \ --upload-file "$file_path" \ --connect-timeout 10 --max-time 120 >/dev/null 2>&1 || continue printf '%s\n' "$(jq -cn \ --arg sid "$sid" \ --arg sha "$sha" \ --argjson size "$fsize" \ --arg s3key "$s3_key" \ --argjson meta "$meta_json" '{ session_id: $sid, sha256: $sha, size_bytes: $size, s3_key: $s3key, metadata_json: $meta }')" >> "${manifest_file}.confirm" total_uploaded=$((total_uploaded + 1)) done # Step 3: Confirm uploads if [ -f "${manifest_file}.confirm" ] && [ -s "${manifest_file}.confirm" ]; then local confirm_json confirm_payload confirm_json=$(jq -s '.' "${manifest_file}.confirm") confirm_payload=$(jq -cn --arg src "$agent_name" --argjson s "$confirm_json" '{ source: $src, sessions: $s }') curl -s -X POST \ "$api_base/v1/installations/$installation_id/agents/$agent_name/sessions/confirm-uploads" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$confirm_payload" >/dev/null 2>&1 || true rm -f "${manifest_file}.confirm" fi # Progress local processed=$((batch_end < session_count ? batch_end : session_count)) echo -e " ${DIM}Processed ${processed}/${session_count} sessions...${RESET}" batch_start=$((batch_end + 1)) done echo -e " ${GREEN}✓${RESET} Session sync: ${total_uploaded} uploaded, ${total_skipped} already synced" } # --- end _common.sh --- dm_banner dm_check_prereqs dm_ensure_dirs # ---- Detect Claude Code ---- CLAUDE_HOME="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" if [ ! -d "$CLAUDE_HOME" ]; then echo -e "${RED}Error:${RESET} Claude Code not found (looked for $CLAUDE_HOME)" echo "Install Claude Code first, then re-run this installer." exit 1 fi echo -e " ${GREEN}✓${RESET} Claude Code detected: ${DIM}$CLAUDE_HOME${RESET}" # ---- Auth ---- dm_auth || { echo -e "${YELLOW}Continuing without authentication. Hook will be installed but not registered.${RESET}" } # ---- Update DM config with agent entry ---- dm_save_agent_config "claude-code" "$CLAUDE_HOME" dm_collect_claude_history_payload() { local claude_home="$1" local projects_dir="$claude_home/projects" local tmp_items local history_items history_meta local index_file_count=0 tmp_items=$(mktemp) if [ -d "$projects_dir" ]; then while IFS= read -r index_file; do [ -z "$index_file" ] && continue index_file_count=$((index_file_count + 1)) jq -c ' (.entries // [])[]? | { session_id: (.sessionId // ""), created: (.created // ""), modified: (.modified // ""), summary: (.summary // ""), first_prompt: (.firstPrompt // ""), project_path: (.projectPath // ""), message_count: (.messageCount // 0) } ' "$index_file" >> "$tmp_items" 2>/dev/null || true done < <(find "$projects_dir" -type f -name 'sessions-index.json' 2>/dev/null) fi if [ -s "$tmp_items" ]; then history_items=$(jq -s 'sort_by((.modified // .created // "")) | reverse | .[:30]' "$tmp_items") else history_items='[]' fi rm -f "$tmp_items" history_meta=$(jq -n \ --argjson items "$history_items" \ --argjson index_files "$index_file_count" ' { source: "claude-projects", session_index_files_used: $index_files, session_files_used: ($items | length), time_range: { from: (($items | map((.created // .modified // "") | tostring) | map(select(length > 0)) | min?) // null), to: (($items | map((.modified // .created // "") | tostring) | map(select(length > 0)) | max?) // null) } } ') jq -cn \ --argjson history_items "$history_items" \ --argjson history_meta "$history_meta" '{ history_items: $history_items, history_meta: $history_meta }' } # ---- Write hook script ---- echo "" echo -e "${BOLD}Installing Claude Code hook...${RESET}" HOOK_SCRIPT="$DM_HOOKS_DIR/claude-code.sh" AGENT_DIR="$DM_HOME/agents/claude-code" mkdir -p "$AGENT_DIR" cat > "$HOOK_SCRIPT" << 'HOOKEOF' #!/usr/bin/env bash # Dialog Machine — Claude Code context hook # Maintains a hot_context.md synced to with.md for the Dialog Machine hub. # Always exits 0 to never block Claude Code. DM_HOME="$HOME/.dialogmachine" DM_CONFIG="$DM_HOME/config.json" AGENT_DIR="$DM_HOME/agents/claude-code" STATE_FILE="$AGENT_DIR/state.json" MD_FILE="$AGENT_DIR/hot_context.md" WITHMD_API="https://with.md/api/public/share" DEFAULT_API_BASE="${DIALOGMACHINE_API_BASE:-https://api.dialogmachine.com}" API_BASE="$DEFAULT_API_BASE" if [ -f "$DM_CONFIG" ]; then api_base_from_config=$(jq -r '.api_base // empty' "$DM_CONFIG" 2>/dev/null) if [ -n "$api_base_from_config" ]; then API_BASE="$api_base_from_config" fi fi mkdir -p "$AGENT_DIR" # Read hook JSON from stdin INPUT=$(cat) EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty') CWD=$(echo "$INPUT" | jq -r '.cwd // empty') # Initialize state file if missing if [ ! -f "$STATE_FILE" ]; then cat > "$STATE_FILE" <<'INIT' { "current_task": null, "completed": [], "session_active": false, "cwd": null } INIT fi # Atomic state update via jq state_update() { local tmp="${STATE_FILE}.tmp.$$" if jq "$@" "$STATE_FILE" > "$tmp" 2>/dev/null; then mv "$tmp" "$STATE_FILE" else rm -f "$tmp" fi } # Render hot_context.md from state render_md() { local state session_active current_task project_dir status project_name now tmp state=$(cat "$STATE_FILE") session_active=$(echo "$state" | jq -r '.session_active') current_task=$(echo "$state" | jq -r '.current_task // empty') project_dir=$(echo "$state" | jq -r '.cwd // empty') status="IDLE" [ "$session_active" = "true" ] && status="ACTIVE" project_name="" [ -n "$project_dir" ] && project_name=$(basename "$project_dir") now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") tmp="${MD_FILE}.tmp.$$" { echo "# Claude Code — Dialog Machine" echo "" echo "**Agent:** claude-code" echo "**Status:** $status" echo "**Updated:** $now" [ -n "$project_name" ] && echo "**Project:** $project_name" echo "" if [ -n "$current_task" ]; then echo "## Currently Working On" echo "" echo "**Human:** $current_task" echo "" fi local completed_count completed_count=$(echo "$state" | jq '.completed | length') if [ "$completed_count" -gt 0 ]; then echo "## Recent Conversations" echo "" echo "$state" | jq -r '.completed | reverse | .[] | "*" + .completed_at + "*\n\n" + "**Human:** " + .task + "\n\n" + "**Claude Code:** " + (if .response != null and .response != "" then .response else "(no response captured)" end) + "\n"' fi echo "---" echo "*Auto-synced by Dialog Machine*" } > "$tmp" mv "$tmp" "$MD_FILE" } # Sync hot_context.md to with.md and store URL in dm config sync_withmd() { local recreate="${1:-}" local content share_id edit_secret [ ! -f "$MD_FILE" ] && return 0 content=$(cat "$MD_FILE") || return 0 share_id="" edit_secret="" if [ -f "$DM_CONFIG" ]; then share_id=$(jq -r '.agents["claude-code"].withmd.share_id // empty' "$DM_CONFIG" 2>/dev/null) edit_secret=$(jq -r '.agents["claude-code"].withmd.edit_secret // empty' "$DM_CONFIG" 2>/dev/null) fi if [ -z "$share_id" ] || [ -z "$edit_secret" ]; then local response http_code body response=$(curl -s -w "\n%{http_code}" -X POST "$WITHMD_API/create" \ -H "Content-Type: application/json" \ --connect-timeout 5 --max-time 10 \ -d "$(jq -n --arg c "$content" \ '{content: $c, title: "Claude Code — Dialog Machine", expiresInHours: 720}')" \ 2>/dev/null) || return 0 http_code=$(echo "$response" | tail -1) body=$(echo "$response" | sed '$d') if [ "$http_code" = "201" ]; then local sid es vu sid=$(echo "$body" | jq -r '.shareId') es=$(echo "$body" | jq -r '.editSecret') vu=$(echo "$body" | jq -r '.viewUrl') if [ -f "$DM_CONFIG" ]; then local tmp="${DM_CONFIG}.tmp.$$" if jq --arg sid "$sid" --arg es "$es" --arg vu "$vu" ' .agents["claude-code"].withmd = {share_id: $sid, edit_secret: $es, view_url: $vu} ' "$DM_CONFIG" > "$tmp" 2>/dev/null; then mv "$tmp" "$DM_CONFIG" else rm -f "$tmp" fi fi fi else local response http_code response=$(curl -s -w "\n%{http_code}" -X PUT "$WITHMD_API/$share_id" \ -H "Content-Type: application/json" \ --connect-timeout 5 --max-time 10 \ -d "$(jq -n --arg c "$content" --arg s "$edit_secret" \ '{editSecret: $s, content: $c, title: "Claude Code — Dialog Machine"}')" \ 2>/dev/null) || return 0 http_code=$(echo "$response" | tail -1) if [ "$http_code" = "404" ] || [ "$http_code" = "403" ]; then if [ "$recreate" != "recreate" ] && [ -f "$DM_CONFIG" ]; then local tmp="${DM_CONFIG}.tmp.$$" jq '.agents["claude-code"].withmd = {share_id: null, edit_secret: null, view_url: null}' \ "$DM_CONFIG" > "$tmp" 2>/dev/null && mv "$tmp" "$DM_CONFIG" sync_withmd "recreate" fi fi fi } # Notify backend notify_backend() { local installation_id machine_token withmd_view_url [ ! -f "$DM_CONFIG" ] && return 0 installation_id=$(jq -r '.installation_id // empty' "$DM_CONFIG" 2>/dev/null) machine_token=$(jq -r '.machine_token // empty' "$DM_CONFIG" 2>/dev/null) withmd_view_url=$(jq -r '.agents["claude-code"].withmd.view_url // empty' "$DM_CONFIG" 2>/dev/null) [ -z "$installation_id" ] && return 0 [ -z "$machine_token" ] && return 0 local heartbeat_payload heartbeat_payload=$(jq -n \ --arg agent "claude-code" \ --arg event "$EVENT" \ --arg withmd "$withmd_view_url" \ --slurpfile state "$STATE_FILE" ' { agent: $agent, event: $event, withmd_view_url: (if $withmd != "" then $withmd else null end), state: ($state[0] // null) } ') curl -s -X POST "$API_BASE/v1/installations/$installation_id/heartbeat" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ -d "$heartbeat_payload" \ --connect-timeout 3 --max-time 5 >/dev/null 2>&1 || true [ ! -f "$MD_FILE" ] && return 0 local content content=$(cat "$MD_FILE") [ -z "$content" ] && return 0 local hot_context_payload hot_context_payload=$(jq -n \ --arg agent "claude-code" \ --arg source "claude-code-hook" \ --arg content "$content" \ --arg withmd "$withmd_view_url" ' { agent: $agent, source: $source, content_markdown: $content, withmd_view_url: (if $withmd != "" then $withmd else null end) } ') curl -s -X PUT "$API_BASE/v1/installations/$installation_id/hot-context" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ -d "$hot_context_payload" \ --connect-timeout 3 --max-time 8 >/dev/null 2>&1 || true } # Periodically refresh bootstrap history (at most once per 24h) maybe_refresh_bootstrap() { [ ! -f "$DM_CONFIG" ] && return 0 local installation_id machine_token installation_id=$(jq -r '.installation_id // empty' "$DM_CONFIG" 2>/dev/null) machine_token=$(jq -r '.machine_token // empty' "$DM_CONFIG" 2>/dev/null) [ -z "$installation_id" ] || [ -z "$machine_token" ] && return 0 # Rate limit: once per 24 hours local last_refresh now_epoch last_refresh=$(jq -r '.last_bootstrap_refresh // 0' "$STATE_FILE" 2>/dev/null) now_epoch=$(date +%s) [ "$((now_epoch - last_refresh))" -lt 86400 ] && return 0 # Collect Claude session history local claude_home="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" local projects_dir="$claude_home/projects" local tmp_items tmp_items=$(mktemp) if [ -d "$projects_dir" ]; then while IFS= read -r index_file; do [ -z "$index_file" ] && continue jq -c ' (.entries // [])[]? | { session_id: (.sessionId // ""), created: (.created // ""), modified: (.modified // ""), summary: (.summary // ""), first_prompt: (.firstPrompt // ""), project_path: (.projectPath // ""), message_count: (.messageCount // 0) } ' "$index_file" >> "$tmp_items" 2>/dev/null || true done < <(find "$projects_dir" -type f -name 'sessions-index.json' 2>/dev/null) fi local history_items if [ -s "$tmp_items" ]; then history_items=$(jq -s 'sort_by((.modified // .created // "")) | reverse | .[:30]' "$tmp_items") else history_items='[]' rm -f "$tmp_items" return 0 fi rm -f "$tmp_items" local item_count item_count=$(printf '%s' "$history_items" | jq 'length' 2>/dev/null || echo "0") [ "$item_count" -eq 0 ] && return 0 local history_meta history_meta=$(jq -n --argjson items "$history_items" '{ source: "claude-projects-refresh", session_files_used: ($items | length), time_range: { from: (($items | map((.created // .modified // "") | tostring) | map(select(length > 0)) | min?) // null), to: (($items | map((.modified // .created // "") | tostring) | map(select(length > 0)) | max?) // null) } }') # Step 1: Summarize via backend local summarize_payload summarize_response http_code body local tmp_hi tmp_hm tmp_hi=$(mktemp); tmp_hm=$(mktemp) printf '%s' "$history_items" > "$tmp_hi" printf '%s' "$history_meta" > "$tmp_hm" summarize_payload=$(jq -n --slurpfile hi "$tmp_hi" --slurpfile hm "$tmp_hm" '{ history_items: ($hi[0] // []), history_meta: ($hm[0] // {}) }') rm -f "$tmp_hi" "$tmp_hm" summarize_response=$(curl -s -w "\n%{http_code}" -X POST \ "$API_BASE/v1/installations/$installation_id/agents/claude-code/bootstrap/summarize" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 60 \ -d "$summarize_payload" 2>/dev/null) || return 0 http_code=$(printf '%s' "$summarize_response" | tail -1) body=$(printf '%s' "$summarize_response" | sed '$d') [ "$http_code" != "200" ] && return 0 local history_summary effective_meta history_summary=$(printf '%s' "$body" | jq -r '.history_summary // ""' 2>/dev/null) effective_meta=$(printf '%s' "$body" | jq -c '.history_meta // {}' 2>/dev/null || echo '{}') # Step 2: Enqueue bootstrap ingestion local tmp_em bootstrap_payload tmp_em=$(mktemp) printf '%s' "$effective_meta" > "$tmp_em" bootstrap_payload=$(jq -n \ --arg history_summary "$history_summary" \ --slurpfile hm "$tmp_em" '{ soul_markdown: "", identity_markdown: "", user_markdown: "", history_summary: $history_summary, history_meta: ($hm[0] // {}) }') rm -f "$tmp_em" curl -s -X POST \ "$API_BASE/v1/installations/$installation_id/agents/claude-code/bootstrap" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$bootstrap_payload" >/dev/null 2>&1 || true # Update timestamp state_update --argjson ts "$now_epoch" '.last_bootstrap_refresh = $ts' } # Periodically sync session files to S3 (at most once per 6h) maybe_sync_sessions() { [ ! -f "$DM_CONFIG" ] && return 0 local installation_id machine_token installation_id=$(jq -r '.installation_id // empty' "$DM_CONFIG" 2>/dev/null) machine_token=$(jq -r '.machine_token // empty' "$DM_CONFIG" 2>/dev/null) [ -z "$installation_id" ] || [ -z "$machine_token" ] && return 0 # Rate limit: once per 6 hours local last_sync now_epoch last_sync=$(jq -r '.last_session_sync // 0' "$STATE_FILE" 2>/dev/null) now_epoch=$(date +%s) [ "$((now_epoch - last_sync))" -lt 21600 ] && return 0 local claude_home="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" local projects_dir="$claude_home/projects" [ ! -d "$projects_dir" ] && return 0 # Build session manifest: collect all session files with SHA256 local manifest_tmp manifest_tmp=$(mktemp) while IFS= read -r index_file; do [ -z "$index_file" ] && continue local project_dir project_dir=$(dirname "$index_file") # Extract session entries with file paths jq -r '.entries[]? | .sessionId + "\t" + .fullPath + "\t" + (.messageCount // 0 | tostring) + "\t" + ((.firstPrompt // "")[:200]) + "\t" + (.projectPath // "") + "\t" + (.gitBranch // "")' \ "$index_file" 2>/dev/null | while IFS=$'\t' read -r sid fpath mcnt fprompt ppath gbranch; do [ -z "$sid" ] || [ -z "$fpath" ] && continue [ ! -f "$fpath" ] && continue local fsize sha fsize=$(wc -c < "$fpath" 2>/dev/null) || continue sha=$(shasum -a 256 < "$fpath" 2>/dev/null | cut -d' ' -f1) || continue [ -z "$sha" ] && continue # Output as JSON line jq -cn \ --arg sid "$sid" \ --arg sha "$sha" \ --argjson size "$fsize" \ --arg fprompt "$fprompt" \ --arg ppath "$ppath" \ --arg gbranch "$gbranch" \ --argjson mcnt "$mcnt" '{ session_id: $sid, sha256: $sha, size_bytes: $size, metadata_json: { first_prompt: $fprompt, project_path: $ppath, git_branch: $gbranch, message_count: $mcnt } }' >> "$manifest_tmp" done done < <(find "$projects_dir" -type f -name 'sessions-index.json' 2>/dev/null) local session_count session_count=$(wc -l < "$manifest_tmp" 2>/dev/null | tr -d ' ') if [ "$session_count" -eq 0 ]; then rm -f "$manifest_tmp" state_update --argjson ts "$now_epoch" '.last_session_sync = $ts' return 0 fi # Process in batches of 100 local batch_start=1 while [ "$batch_start" -le "$session_count" ]; do local batch_end=$((batch_start + 99)) local batch_json batch_json=$(sed -n "${batch_start},${batch_end}p" "$manifest_tmp" | jq -s '.') # Step 1: Get upload URLs local urls_payload urls_response urls_code urls_body urls_payload=$(jq -cn --argjson s "$batch_json" '{ sessions: $s }') urls_response=$(curl -s -w "\n%{http_code}" -X POST \ "$API_BASE/v1/installations/$installation_id/agents/claude-code/sessions/upload-urls" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$urls_payload" 2>/dev/null) || { batch_start=$((batch_end + 1)); continue; } urls_code=$(printf '%s' "$urls_response" | tail -1) urls_body=$(printf '%s' "$urls_response" | sed '$d') [ "$urls_code" != "200" ] && { batch_start=$((batch_end + 1)); continue; } # Step 2: Upload each non-skipped session local confirm_items='[]' local uploaded=0 printf '%s' "$urls_body" | jq -c '.results[]? | select(.skipped == false)' 2>/dev/null | while IFS= read -r item; do local sid upload_url s3_key sid=$(printf '%s' "$item" | jq -r '.session_id') upload_url=$(printf '%s' "$item" | jq -r '.upload_url') s3_key=$(printf '%s' "$item" | jq -r '.s3_key') [ -z "$upload_url" ] || [ "$upload_url" = "null" ] && continue # Find the session file path and metadata from manifest local manifest_line manifest_line=$(jq -c "select(.session_id == \"$sid\")" "$manifest_tmp" 2>/dev/null | head -1) [ -z "$manifest_line" ] && continue local fpath sha fsize meta_json sha=$(printf '%s' "$manifest_line" | jq -r '.sha256') fsize=$(printf '%s' "$manifest_line" | jq -r '.size_bytes') meta_json=$(printf '%s' "$manifest_line" | jq -c '.metadata_json') # Find the actual file - search for session file fpath=$(find "$claude_home/projects" -name "${sid}.jsonl" -type f 2>/dev/null | head -1) [ -z "$fpath" ] || [ ! -f "$fpath" ] && continue # Upload to S3 via pre-signed URL curl -s -X PUT "$upload_url" \ -H "Content-Type: application/x-ndjson" \ --upload-file "$fpath" \ --connect-timeout 10 --max-time 120 >/dev/null 2>&1 || continue # Add to confirm list printf '%s\n' "$(jq -cn \ --arg sid "$sid" \ --arg sha "$sha" \ --argjson size "$fsize" \ --arg s3key "$s3_key" \ --argjson meta "$meta_json" '{ session_id: $sid, sha256: $sha, size_bytes: $size, s3_key: $s3key, metadata_json: $meta }')" >> "${manifest_tmp}.confirm" done # Step 3: Confirm uploads if [ -f "${manifest_tmp}.confirm" ] && [ -s "${manifest_tmp}.confirm" ]; then local confirm_json confirm_json=$(jq -s '.' "${manifest_tmp}.confirm") local confirm_payload confirm_payload=$(jq -cn --argjson s "$confirm_json" '{ source: "claude-code", sessions: $s }') curl -s -X POST \ "$API_BASE/v1/installations/$installation_id/agents/claude-code/sessions/confirm-uploads" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$confirm_payload" >/dev/null 2>&1 || true rm -f "${manifest_tmp}.confirm" fi batch_start=$((batch_end + 1)) done rm -f "$manifest_tmp" state_update --argjson ts "$now_epoch" '.last_session_sync = $ts' } # Periodically sync Codex session files to S3 (at most once per 6h) maybe_sync_codex_sessions() { [ ! -f "$DM_CONFIG" ] && return 0 local installation_id machine_token installation_id=$(jq -r '.installation_id // empty' "$DM_CONFIG" 2>/dev/null) machine_token=$(jq -r '.machine_token // empty' "$DM_CONFIG" 2>/dev/null) [ -z "$installation_id" ] || [ -z "$machine_token" ] && return 0 # Rate limit: once per 6 hours local last_sync now_epoch last_sync=$(jq -r '.last_codex_session_sync // 0' "$STATE_FILE" 2>/dev/null) now_epoch=$(date +%s) [ "$((now_epoch - last_sync))" -lt 21600 ] && return 0 local codex_sessions_dir="${CODEX_HOME:-$HOME/.codex}/sessions" [ ! -d "$codex_sessions_dir" ] && return 0 # Build session manifest from Codex JSONL files local manifest_tmp manifest_tmp=$(mktemp) find "$codex_sessions_dir" -type f -name "*.jsonl" 2>/dev/null | while IFS= read -r fpath; do [ -z "$fpath" ] && continue local fname fname=$(basename "$fpath" .jsonl) # Extract ULID session ID from filename: rollout-YYYY-MM-DDTHH-MM-SS-{ULID} local sid sid=$(echo "$fname" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') [ -z "$sid" ] && sid="$fname" local fsize sha fsize=$(wc -c < "$fpath" 2>/dev/null) || continue sha=$(shasum -a 256 < "$fpath" 2>/dev/null | cut -d' ' -f1) || continue [ -z "$sha" ] && continue # Extract cwd from session_meta (first line) local cwd_path="" cwd_path=$(head -1 "$fpath" 2>/dev/null | jq -r '.payload.cwd // empty' 2>/dev/null) jq -cn \ --arg sid "$sid" \ --arg sha "$sha" \ --argjson size "$fsize" \ --arg ppath "$cwd_path" '{ session_id: $sid, sha256: $sha, size_bytes: $size, metadata_json: { first_prompt: "", project_path: $ppath, git_branch: "", message_count: 0 } }' >> "$manifest_tmp" done local session_count session_count=$(wc -l < "$manifest_tmp" 2>/dev/null | tr -d ' ') if [ "$session_count" -eq 0 ]; then rm -f "$manifest_tmp" state_update --argjson ts "$now_epoch" '.last_codex_session_sync = $ts' return 0 fi # Process in batches of 100 local batch_start=1 while [ "$batch_start" -le "$session_count" ]; do local batch_end=$((batch_start + 99)) local batch_json batch_json=$(sed -n "${batch_start},${batch_end}p" "$manifest_tmp" | jq -s '.') local urls_payload urls_response urls_code urls_body urls_payload=$(jq -cn --argjson s "$batch_json" '{ sessions: $s }') urls_response=$(curl -s -w "\n%{http_code}" -X POST \ "$API_BASE/v1/installations/$installation_id/agents/codex/sessions/upload-urls" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$urls_payload" 2>/dev/null) || { batch_start=$((batch_end + 1)); continue; } urls_code=$(printf '%s' "$urls_response" | tail -1) urls_body=$(printf '%s' "$urls_response" | sed '$d') [ "$urls_code" != "200" ] && { batch_start=$((batch_end + 1)); continue; } printf '%s' "$urls_body" | jq -c '.results[]? | select(.skipped == false)' 2>/dev/null | while IFS= read -r item; do local sid upload_url s3_key sid=$(printf '%s' "$item" | jq -r '.session_id') upload_url=$(printf '%s' "$item" | jq -r '.upload_url') s3_key=$(printf '%s' "$item" | jq -r '.s3_key') [ -z "$upload_url" ] || [ "$upload_url" = "null" ] && continue local manifest_line manifest_line=$(jq -c "select(.session_id == \"$sid\")" "$manifest_tmp" 2>/dev/null | head -1) [ -z "$manifest_line" ] && continue local sha fsize meta_json sha=$(printf '%s' "$manifest_line" | jq -r '.sha256') fsize=$(printf '%s' "$manifest_line" | jq -r '.size_bytes') meta_json=$(printf '%s' "$manifest_line" | jq -c '.metadata_json') # Find the actual file local fpath fpath=$(find "$codex_sessions_dir" -name "*${sid}*" -type f 2>/dev/null | head -1) [ -z "$fpath" ] || [ ! -f "$fpath" ] && continue curl -s -X PUT "$upload_url" \ -H "Content-Type: application/x-ndjson" \ --upload-file "$fpath" \ --connect-timeout 10 --max-time 120 >/dev/null 2>&1 || continue printf '%s\n' "$(jq -cn \ --arg sid "$sid" \ --arg sha "$sha" \ --argjson size "$fsize" \ --arg s3key "$s3_key" \ --argjson meta "$meta_json" '{ session_id: $sid, sha256: $sha, size_bytes: $size, s3_key: $s3key, metadata_json: $meta }')" >> "${manifest_tmp}.confirm" done if [ -f "${manifest_tmp}.confirm" ] && [ -s "${manifest_tmp}.confirm" ]; then local confirm_json confirm_json=$(jq -s '.' "${manifest_tmp}.confirm") local confirm_payload confirm_payload=$(jq -cn --argjson s "$confirm_json" '{ source: "codex", sessions: $s }') curl -s -X POST \ "$API_BASE/v1/installations/$installation_id/agents/codex/sessions/confirm-uploads" \ -H "Authorization: Bearer $machine_token" \ -H "Content-Type: application/json" \ --connect-timeout 8 --max-time 30 \ -d "$confirm_payload" >/dev/null 2>&1 || true rm -f "${manifest_tmp}.confirm" fi batch_start=$((batch_end + 1)) done rm -f "$manifest_tmp" state_update --argjson ts "$now_epoch" '.last_codex_session_sync = $ts' } # ---- Event Dispatch ---- case "$EVENT" in UserPromptSubmit) PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty') TASK=$(echo "$PROMPT" | head -1 | cut -c1-120) if [ -n "$TASK" ]; then NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") state_update --arg task "$TASK" --arg now "$NOW" --arg cwd "${CWD:-}" ' (if .current_task != null and .current_task != "" then .completed += [{"task": .current_task, "response": null, "completed_at": $now}] else . end) | .current_task = $task | .cwd = (if $cwd != "" then $cwd else .cwd end) | .session_active = true | .completed = (.completed | .[-3:]) ' fi render_md ( sync_withmd ) >/dev/null 2>&1 & ;; Stop) STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false') [ "$STOP_ACTIVE" = "true" ] && exit 0 LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty') NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") state_update --arg now "$NOW" --arg resp "$LAST_MSG" ' (if .current_task != null and .current_task != "" then .completed += [{"task": .current_task, "response": $resp, "completed_at": $now}] | .current_task = null else . end) | .completed = (.completed | .[-3:]) ' render_md ( sync_withmd ) >/dev/null 2>&1 & ;; SessionStart) state_update --arg cwd "${CWD:-}" ' .session_active = true | .cwd = (if $cwd != "" then $cwd else .cwd end) ' render_md ( sync_withmd ) >/dev/null 2>&1 & ( maybe_refresh_bootstrap ) >/dev/null 2>&1 & ( maybe_sync_sessions ) >/dev/null 2>&1 & ( maybe_sync_codex_sessions ) >/dev/null 2>&1 & ;; SessionEnd) LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty') NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") state_update --arg now "$NOW" --arg resp "$LAST_MSG" ' .session_active = false | (if .current_task != null and .current_task != "" then .completed += [{"task": .current_task, "response": $resp, "completed_at": $now}] | .current_task = null else . end) | .completed = (.completed | .[-3:]) ' render_md sync_withmd # Synchronous on session exit ;; *) ;; esac ( notify_backend ) >/dev/null 2>&1 & exit 0 HOOKEOF chmod +x "$HOOK_SCRIPT" echo -e " ${GREEN}✓${RESET} Hook script: $HOOK_SCRIPT" # ---- Update Claude Code settings.json ---- CLAUDE_SETTINGS="$CLAUDE_HOME/settings.json" DM_HOOK_CONFIG=$(cat < "$tmp" mv "$tmp" "$CLAUDE_SETTINGS" echo -e " ${GREEN}✓${RESET} Updated: $CLAUDE_SETTINGS ${DIM}(hooks merged)${RESET}" else mkdir -p "$(dirname "$CLAUDE_SETTINGS")" jq -n --argjson hooks "$DM_HOOK_CONFIG" '{hooks: $hooks}' > "$CLAUDE_SETTINGS" echo -e " ${GREEN}✓${RESET} Created: $CLAUDE_SETTINGS" fi # Mark hook installed in config dm_save_config '.agents["claude-code"].hook_installed = true' # ---- Register agent with backend ---- WITHMD_VIEW_URL=$(dm_read_config '.agents["claude-code"].withmd.view_url') dm_register_agent "claude-code" "$CLAUDE_HOME" "true" "${WITHMD_VIEW_URL:-}" # ---- Bootstrap ingestion ---- echo "" echo -e "${BOLD}Preparing Claude bootstrap context...${RESET}" CLAUDE_BOOTSTRAP_PAYLOAD=$(dm_collect_claude_history_payload "$CLAUDE_HOME") CLAUDE_HISTORY_ITEMS=$(printf '%s' "$CLAUDE_BOOTSTRAP_PAYLOAD" | jq -c '.history_items // []' 2>/dev/null || echo "[]") CLAUDE_HISTORY_META=$(printf '%s' "$CLAUDE_BOOTSTRAP_PAYLOAD" | jq -c '.history_meta // {}' 2>/dev/null || echo "{}") dm_run_agent_bootstrap_flow "claude-code" "$CLAUDE_HISTORY_ITEMS" "$CLAUDE_HISTORY_META" "" "" # ---- Session ingestion (S3 upload + summarization) ---- echo "" echo -e "${BOLD}Uploading Claude Code sessions...${RESET}" DM_SESSION_MANIFEST=$(mktemp) CLAUDE_PROJECTS_DIR="$CLAUDE_HOME/projects" # Collect sessions from indexes + direct scan if [ -d "$CLAUDE_PROJECTS_DIR" ]; then DM_SEEN_SIDS="" # From sessions-index.json while IFS= read -r index_file; do [ -z "$index_file" ] && continue jq -r '.entries[]? | .sessionId + "\t" + .fullPath + "\t" + ((.firstPrompt // "")[:200]) + "\t" + (.projectPath // "") + "\t" + (.gitBranch // "")' \ "$index_file" 2>/dev/null | while IFS=$'\t' read -r sid fpath fprompt ppath gbranch; do [ -z "$sid" ] || [ -z "$fpath" ] || [ ! -f "$fpath" ] && continue echo "$sid" | grep -qF "$DM_SEEN_SIDS" 2>/dev/null && continue fsize=$(wc -c < "$fpath" 2>/dev/null) || continue sha=$(shasum -a 256 < "$fpath" 2>/dev/null | cut -d' ' -f1) || continue [ -z "$sha" ] && continue jq -cn --arg sid "$sid" --arg sha "$sha" --argjson size "$fsize" --arg fp "$fpath" \ --arg fprompt "$fprompt" --arg ppath "$ppath" --arg gbranch "$gbranch" '{ session_id: $sid, sha256: $sha, size_bytes: $size, file_path: $fp, metadata_json: {first_prompt: $fprompt, project_path: $ppath, git_branch: $gbranch, message_count: 0} }' >> "$DM_SESSION_MANIFEST" done done < <(find "$CLAUDE_PROJECTS_DIR" -type f -name 'sessions-index.json' 2>/dev/null) # Direct scan for .jsonl files while IFS= read -r jsonl_file; do [ -z "$jsonl_file" ] && continue echo "$jsonl_file" | grep -q '/subagents/' && continue sid=$(basename "$jsonl_file" .jsonl) # Skip if already in manifest grep -q "\"session_id\":\"$sid\"" "$DM_SESSION_MANIFEST" 2>/dev/null && continue fsize=$(wc -c < "$jsonl_file" 2>/dev/null) || continue sha=$(shasum -a 256 < "$jsonl_file" 2>/dev/null | cut -d' ' -f1) || continue [ -z "$sha" ] && continue ppath=$(basename "$(dirname "$jsonl_file")") jq -cn --arg sid "$sid" --arg sha "$sha" --argjson size "$fsize" --arg fp "$jsonl_file" \ --arg ppath "$ppath" '{ session_id: $sid, sha256: $sha, size_bytes: $size, file_path: $fp, metadata_json: {first_prompt: "", project_path: $ppath, git_branch: "", message_count: 0} }' >> "$DM_SESSION_MANIFEST" done < <(find "$CLAUDE_PROJECTS_DIR" -maxdepth 2 -type f -name '*.jsonl' 2>/dev/null) fi dm_sync_sessions_to_s3 "claude-code" "$DM_SESSION_MANIFEST" rm -f "$DM_SESSION_MANIFEST" # ---- Summary ---- echo "" echo -e "${BOLD}${GREEN}Done!${RESET}" echo "" echo -e " Config: ${DIM}$DM_CONFIG${RESET}" echo -e " Hook: ${DIM}$HOOK_SCRIPT${RESET}" echo "" echo -e " ${YELLOW}Restart Claude Code for hooks to take effect.${RESET}" echo ""